#!/bin/bash

# ------------------------------------
# 配置、变量获取、初始化类
#
# 读取设置文件
get_config() {
	while [[ "$*" != "" ]]; do
		eval "${1}='$(uci get wechatpush.config.$1 2>/dev/null)'"
		shift
	done
}

# 后台读取 json 中的变量
read_json() {
	local json_key="$1"
	local json_path="$2"
	local output_file="$output_dir/${json_key}"
	jq -r ."$json_key" "$json_path" >"$output_file" &
}

# 遍历输出目录，将文件内容保存到对应的变量
wait_and_cat() {
	[ $(ls -A "$output_dir" | wc -l) -eq 0 ] && return
	wait
	for file in "$output_dir"/*; do
		local variable_name=$(basename "$file")
		local variable_value=$(cat "$file")
		eval "${variable_name}='${variable_value}'"
	done
	if [ ! -f "${dir}/send_pid" ]; then
		rm "$output_dir"/*
	fi
}

# 初始化设置信息
read_config() {
	get_config \
		"enable" "lite_enable" "device_name" "proxy_address" "sleeptime" "oui_data" "reset_regularly" "debuglevel" \
		"jsonpath" "sckey" "sc3key" "sc3uid" "sc3tags" "corpid" "userid" "agentid" "corpsecret" "mediapath" "wxpusher_apptoken" "wxpusher_uids" "wxpusher_topicIds" "pushplus_token" "tg_token" "chat_id" "recipient_email" \
		"get_ipv4_mode" "ipv4_interface" "get_ipv6_mode" "ipv6_interface" \
		"device_notification" "cpu_notification" "cpu_load_threshold" "temperature_threshold" \
		"client_usage" "client_usage_max" "client_usage_disturb" "client_usage_whitelist" \
		"login_notification" "login_max_num" "login_web_black" "login_ip_black_timeout" "login_ip_white_list" "port_knocking_enable" "login_ip_white_timeout" "login_port_white" "login_port_forward_list" \
		"crontab_regular_time" "crontab_interval_time" \
		"do_not_disturb_mode" "do_not_disturb_starttime" "do_not_disturb_endtime" "up_down_push_whitelist" "up_down_push_blacklist" "up_down_push_interface" "mac_online_list" "mac_offline_list" "cpu_threshold_duration" "cpu_notification_delay" "login_disturb" "login_notification_delay" "login_log_enable" \
		"up_timeout" "down_timeout" "timeout_retry_count" "always_check_ip_list" "only_timeout_push" "passive_mode" "thread_num" "defaultSortColumn" "soc_code" "server_host" "server_port" \
		"unattended_enable" "zerotier_helper" "unattended_only_on_disturb_time" "unattended_device_aliases" "network_disconnect_event" "unattended_autoreboot_mode" "autoreboot_system_uptime" "autoreboot_network_uptime" \
		"device_info_helper" "gateway_host_url" "gateway_info_url" "gateway_logout_url" "gateway_username_id" "gateway_password_id" "gateway_username" "gateway_password" "miwifi_ip" "miwifi_password" "mikrotik_ip" "mikrotik_username" "op_host_ips" "scan_ip_range" "device_info_helper_sleeptime"

	(echo "$device_notification" | grep -q "online") && notification_online="true"
	(echo "$device_notification" | grep -q "offline") && notification_offline="true"
	(echo "$cpu_notification" | grep -q "load") && notification_load="true"
	(echo "$cpu_notification" | grep -q "temp") && notification_temp="true"
	(echo "$login_notification" | grep -q "web_logged") && web_logged="true"
	(echo "$login_notification" | grep -q "ssh_logged") && ssh_logged="true"
	(echo "$login_notification" | grep -q "web_login_failed") && web_login_failed="true"
	(echo "$login_notification" | grep -q "ssh_login_failed") && ssh_login_failed="true"
	(echo "$device_info_helper" | grep -q "gateway_info") && gateway_info_enable="true"
	(echo "$device_info_helper" | grep -q "miwifi_info") && miwifi_info_enable="true"
	(echo "$device_info_helper" | grep -q "mikrotik_info") && mikrotik_info_enable="true"
	(echo "$device_info_helper" | grep -q "openwrt_info") && openwrt_info_enable="true"
	op_host_ips=$(echo "$op_host_ips" | sed 's/ /\n/g') 2>/dev/null
	for str_version in "wrtbwmon" "iputils-arping" "curl" "iw"; do
		eval $(echo ${str_version:0:2}"_version")=$(opkg list-installed | grep -w ^${str_version} | awk '{print $3}') 2>/dev/null
	done
	(opkg list-installed | grep -w -q "^firewall4") && nftables_version="true"
	devices_json="${dir}/devices.json"
	tmp_devices_json="${dir}/devices.json.tmp"
	tempjsonpath="${dir}/temp.json"
	ip_blacklist_path="/usr/share/wechatpush/api/ip_blacklist"
	oui_base="${dir}/oui_base.txt"
	[ -z "$debuglevel" ] && logfile="/dev/null" || logfile="${dir}/wechatpush.log"
	login_port_forward_list=$(echo "$login_port_forward_list" | sed 's/ /\n/g') 2>/dev/null
	up_down_push_blacklist=$(echo "$up_down_push_blacklist" | sed 's/ /\n/g') 2>/dev/null
	up_down_push_whitelist=$(echo "$up_down_push_whitelist" | sed 's/ /\n/g') 2>/dev/null
	device_aliases_path="/usr/share/wechatpush/api/device_aliases.list"
	always_check_ip_list=$(echo "$always_check_ip_list" | sed 's/ /\n/g') 2>/dev/null
	unattended_device_aliases=$(echo "$unattended_device_aliases" | sed 's/ /\n/g') 2>/dev/null
	client_usage_whitelist=$(echo "$client_usage_whitelist" | sed 's/ /\n/g') 2>/dev/null
	login_ip_white_list=$(echo "$login_ip_white_list" | sed 's/ /\n/g') 2>/dev/null
	mark_mac_list="${mac_online_list} ${mac_offline_list}"
	mark_mac_list=$(echo "$mark_mac_list" | sed 's/ /\n/g' | sed 's/-/ /') 2>/dev/null
	ipv4_urllist=$(cat /usr/share/wechatpush/api/ipv4.list) 2>/dev/null
	ipv6_urllist=$(cat /usr/share/wechatpush/api/ipv6.list) 2>/dev/null
	lang=$(echo "${LANG:-${LANGUAGE:-${LC_ALL:-${LC_MESSAGES:-zh_cn}}}}" | awk -F'[ .@]' '{print tolower($1)}' | sed 's/-/_/') 2>/dev/null
	[ -z "$get_ipv4_mode" ] && get_ipv4_mode=0
	[ -z "$get_ipv6_mode" ] && get_ipv6_mode=0
	[ -z "$sleeptime" ] && sleeptime="60"
	[ -z "$login_ip_black_timeout" ] && login_ip_black_timeout="86400"
	[ -z "$login_ip_white_timeout" ] && login_ip_white_timeout="600"
	[ "$iw_version" ] && wlan_interface=$(iw dev 2>/dev/null | grep Interface | awk '{print $2}') >/dev/null 2>&1
	[ -z "$server_port" ] && server_port="22"
	output_dir="${dir}/json_output"
	mkdir -p "$output_dir"
	if (echo "$lite_enable" | grep -q "content") || [ -z "$jsonpath" ]; then
		str_title_start="" && str_title_end="" && str_splitline="" && str_linefeed="" && str_tab=""
	else
		read_json "str_title_start" "$jsonpath"
		read_json "str_title_end" "$jsonpath"
		read_json "str_linefeed" "$jsonpath"
		read_json "str_splitline" "$jsonpath"
		read_json "str_space" "$jsonpath"
		read_json "str_tab" "$jsonpath"
		read_json "_api" "$jsonpath"
	fi
	wait_and_cat
	disturb_text=$_api
	deltemp
	cron
}

# 初始化
init() {
	# 检测程序开关
	enable_detection
	[ -f "$logfile" ] && local logrow=$(grep -c "" "$logfile") || local logrow="0"
	[ "$logrow" -ne 0 ] && echo "---------------------------------------------------------------------------------------" >>${logfile}
	log_change "[INFO] $(translate "Start running...")"
	if [ -f "/usr/share/wechatpush/errlog" ]; then
		cat /usr/share/wechatpush/errlog >${logfile}
		log_change "[ERROR] $(translate "Loaded logs from previous restart")"
	fi

	# 配置检查
	[ ! -f "/usr/sbin/wrtbwmon" ] && log_change "[WARN] $(translate "wrtbwmon not installed, traffic statistics unavailable")"
	[ -z "$ip_version" ] && log_change "[ERROR] $(translate "Cannot get iputils-arping version, please confirm if plugin is running properly")"
	[ -z "$cu_version" ] && log_change "[ERROR] $(translate "Cannot get curl version, please confirm if plugin is running properly")"

	[ -n "$jsonpath" ] && [ -z "${sckey}${sc3key}${sc3uid}${sc3tags}${tg_token}${pushplus_token}${corpid}${wxpusher_apptoken}${wxpusher_uids}${wxpusher_topicIds}" -a "${jsonpath}" != "/usr/share/wechatpush/api/diy.json" ] && log_change "[ERROR] $(translate "Please fill in the correct API key")" && return 1

	local interfacelist=$(getinterfacelist) && [ -z "$interfacelist" ] && log_change "[WARN] $(translate "Multiple interfaces detected or configuration error, may not get interface uptime information, please confirm if plugin is running properly")"

	[ -n "$notification_temp" ] && [ -n "$temperature_threshold" ] && local cpu_temp=$(soc_temp) || local cpu_temp="null"
	[ -z "$cpu_temp" ] && log_change "[WARN] $(translate "Cannot read device temperature, please check command")"

	[ -n "$notification_load" ] && [ -n "$cpu_load_threshold" ] && local cpu_load=$(cat /proc/loadavg | awk '{print $1}') 2>/dev/null || local cpu_load="null"
	[ -z "$cpu_load" ] && log_change "[WARN] $(translate "Cannot read device load, please check command")"

	# 检查 JSON 文件格式是否有效
	if ! jq empty "$devices_json" >/dev/null 2>&1 || ! jq -e 'has("address") and has("devices") and has("disks")' "$devices_json" >/dev/null 2>&1; then
		[ -f "$devices_json" ] && log_change "[WARN] $(translate "Device list file format error, needs reinitialization. Original file saved to ${devices_json}.err")" && mv "$devices_json" "${devices_json}.err"
	fi
	# 如果设备列表文件为空，初始化
	[ ! -s "$devices_json" ] && echo '{"address": [], "devices": [], "disks": []}' >"$devices_json"

	# 判断 WLAN 接口频段
	if [ -n "$wlan_interface" ]; then
		# 初始化一个空的 JSON 数组
		local wlan_info="[]"

		# 遍历每个接口
		for iface in $wlan_interface; do
			local info=$(iw dev $iface info 2>/dev/null)
			local channel=$(echo "$info" | grep "channel" | awk '{print $2}')
			local band="unknown"

			# 判断频段
			if [[ -n "$channel" ]]; then
				if [[ "$channel" -ge 1 && "$channel" -le 14 ]]; then
					band="2.4G"
				elif [[ "$channel" -ge 36 ]]; then
					band="5G"
				fi
			fi

			# 将接口信息添加到 JSON 数组
			wlan_info=$(echo "$wlan_info" | jq --arg iface "$iface" --arg band "$band" '. += [{"interface": $iface, "band": $band}]')
		done

		jq --argjson wlan_info "$wlan_info" '.wlan = $wlan_info' "$devices_json" > "${devices_json}.tmp" && mv "${devices_json}.tmp" "$devices_json"
	fi

	# 文件下载
	/usr/libexec/wechatpush-call "down_oui"

	# 文件清理
	rm -f "${dir}/sheep_usage" "${dir}/old_sheep_usage" "${dir}/client_usage_aliases" "${dir}/old_client_usage_aliases" "/usr/share/wechatpush/errlog" "${dir}/temp_err" >/dev/null 2>&1
	LockFile unlock

	# 防火墙初始化
	[ -n "$login_web_black" ] && [ "$login_web_black" -eq "1" ] && init_ip_black "ipv4"
	[ -n "$login_web_black" ] && [ "$login_web_black" -eq "1" ] && init_ip_black "ipv6"
	[ -n "$port_knocking_enable" ] && [ "$port_knocking_enable" -eq "1" ] && init_ip_white "ipv4"
	[ -n "$port_knocking_enable" ] && [ "$port_knocking_enable" -eq "1" ] && init_ip_white "ipv6"
	tmp_ip_list=$(echo "$login_ip_white_list" | grep -v "^$" | sort -u)
	while IFS= read -r tmp_ip; do
		[ -n "$tmp_ip" ] && add_ip_white "$tmp_ip" "0"
	done <<<"$tmp_ip_list"
	set_ip_black

	return 0
}

# 创建计划任务
cron() {
	del_cron() {
		crontab -l | grep -v "wechatpush" | crontab -
	}
	re_cron() {
		/etc/init.d/cron stop
		/etc/init.d/cron start
	}
	del_cron
	if [ -z "$enable" ]; then
		re_cron
		return
	fi

	# 重置流量
	if [ -n "$reset_regularly" ] && [ "$reset_regularly" -eq "1" ]; then
		(
			crontab -l 2>/dev/null
			echo "0 0 * * * rm ${dir}/usage.db >/dev/null 2>&1"
		) | crontab -
		(
			crontab -l 2>/dev/null
			echo "0 0 * * * rm ${dir}/usage6.db >/dev/null 2>&1"
		) | crontab -
	fi

	# 定时发送
	if [ -n "$crontab_regular_time" ]; then
		crontab_regular_time=$(echo "$crontab_regular_time" | sed 's/ /,/g')
		(
			crontab -l 2>/dev/null
			echo "0 $crontab_regular_time * * * /usr/share/wechatpush/wechatpush send &"
		) | crontab -
	# 间隔发送
	elif [ -n "$crontab_interval_time" ]; then
		(
			crontab -l 2>/dev/null
			echo "0 */$crontab_interval_time * * * /usr/share/wechatpush/wechatpush send &"
		) | crontab -
	fi

	re_cron
}

# ------------------------------------
# 主程序
main() {
	# 限制并发进程
	dir="/tmp/wechatpush" && mkdir -p ${dir}
	get_config "thread_num"
	[ -z "$thread_num" ] || [ "$thread_num" -eq "0" ] && thread_num=5
	[ "$1" ] && [ $1 == "t1" ] && thread_num=1
	max_thread_num="$thread_num"

	FIFO_PATH="${dir}/fifo.$$"
	mkfifo "$FIFO_PATH"
	exec 5<>"$FIFO_PATH"
	rm "$FIFO_PATH" >/dev/null 2>&1

	for i in $(seq 1 "$max_thread_num"); do
		echo >&5
	done
	unset i

	# 定义锁文件
	lock_file="${dir}/wechatpush.lock"
	touch "$lock_file"

	# 设置信号处理
	trap cleanup SIGINT SIGTERM EXIT
	MAIN_PID=$$
	PROCESS_TAG="{wechatpush}_${MAIN_PID}"

	# 初始化
	if [ "$1" ]; then
		[ $1 == "soc" ] && {
			get_config "soc_code" "server_host" "server_port"

			if [ -n "$server_host" ]; then
				if [ ! -f "${dir}/temp_err" ]; then
					cputemp=$(soc_temp)
					if [ -n "$cputemp" ] && [[ "$cputemp" != "0.0" ]]; then
						echo "$cputemp" && exit
					else
						>"${dir}/temp_err"
						get_config "debuglevel"
						[ -z "$debuglevel" ] && logfile="/dev/null" || logfile="${dir}/wechatpush.log"
						log_change "[ERROR] $(translate "Failed to retrieve remote host temperature. To prevent SSH connection timeout from affecting program functionality, the temperature retrieval feature has been disabled. Please verify your configuration and restart the script, or manually delete /tmp/wechatpush/temp_err to retry.")"
					fi
				fi
				exit 1
			else
				soc_temp && exit $?
			fi
		}
		[ $1 == "getip" ] && {
			get_config "get_ipv4_mode" "ipv4_interface" "get_ipv6_mode" "ipv6_interface"
			ipv4_urllist=$(cat /usr/share/wechatpush/api/ipv4.list) 2>/dev/null
			ipv6_urllist=$(cat /usr/share/wechatpush/api/ipv6.list) 2>/dev/null
			output_dir="${dir}/json_output" && mkdir -p "$output_dir"
			ip_changes getip && exit $?
		}
		silent_run read_config
		[ "$1" == "send" ] && {
			send
			exit $?
		}
		[ "$1" == "test" ] && {
			send test
			exit $?
		}
	else
		silent_run read_config
	fi

	# 载入在线设备
	init
	[ $? -eq 1 ] && log_change "[ERROR] $(translate "Failed to read settings, please check configuration.")" && exit

	log_change "[INFO] $(translate "Loading online devices...")"
	silent_run monitor_logins
	>"${dir}/send_enable.lock" && first && deltemp
	log_change "[INFO] $(translate "Initialization completed")"

	# 循环
	while [ "$enable" -eq "1" ]; do
		deltemp
		usage update

		# 网络状态与 IP 变动
		if [ "$get_ipv4_mode" -ne "0" ] || [ "$get_ipv6_mode" -ne "0" ]; then
			check_connect
			ip_changes
		fi

		# 设备列表
		if [ ! -f "${dir}/send_enable.lock" ]; then
			[ -n "$title" ] && echo "$title" >"${dir}/title"
			[ -n "$content" ] && echo "$content" >"${dir}/content"
			first
			[ -f "${dir}/title" ] && title=$(cat "${dir}/title") && rm -f "${dir}/title" >/dev/null 2>&1
			[ -f "${dir}/content" ] && content=$(cat "${dir}/content") && rm -f "${dir}/content" >/dev/null 2>&1
		fi

		# 离线二次验证区推送
		[ ! -f "${dir}/send_enable.lock" ] && down_send

		# 当前设备列表
		[ -n "$content" ] && [ ! -f "${dir}/send_enable.lock" ] && current_device

		# 无人值守任务
		[ ! -f "${dir}/send_enable.lock" ] && unattended

		# CPU 检测
		[ ! -f "${dir}/send_enable.lock" ] && check_cpu_load

		# 硬盘检测
		#[ ! -f "${dir}/send_enable.lock" ] && get_disk

		# 异常流量检测
		[ ! -f "${dir}/send_enable.lock" ] && get_client_usage

		# 登录提醒通知
		#[ ! -f "${dir}/send_enable.lock" ] && login_send
		# 因修改为实时推送，白名单设备并不会再更新时间，暂时性修复
		# 修改为实时推送后，防火墙列表读取和设置需单独列出，移除黑名单无法实时，待改善
		set_ip_black

		tmp_ip_list=$(echo "$login_ip_white_list" | grep -v "^$" | sort -u)
		while IFS= read -r tmp_ip; do
			[ -n "$tmp_ip" ] && add_ip_white "$tmp_ip" "0"
		done <<<"$tmp_ip_list"

		# 推送
		if [ ! -f "${dir}/send_enable.lock" ] && [ -n "$title" ] && [ -n "$content" ]; then
			[ -n "$device_name" ] && title="【$device_name】$title"
			(echo "$lite_enable" | grep -q "content") && content="$title"
			disturb
			disturb_RETVAL=$?
			[ "$disturb_RETVAL" -eq 0 ] && diy_send "${title}" "${content}" "${jsonpath}" >/dev/null 2>&1
		fi

		sleep $sleeptime

		# 等待定时任务推送完成
		while [ -f "${dir}/send_pid" ]; do
			pid=$(cat "${dir}/send_pid")
			if ps | grep "^\s*${pid}\s" >/dev/null; then
				sleep 1
			else
				rm -f "${dir}/send_pid"
				break
			fi
		done
	done
}

# ------------------------------------
# 工具函数类
#
# 在调试模式下隐藏输出
# 不能直接包裹 var=$(echo $ssh_command) 等命令，待完善
silent_run() {
	"$@" >/dev/null 2>&1
}

# 处理并检查 tmp_name 的通用函数
process_and_check() {
	[ -z "$1" ] && return 1
	local value=$(echo "$1" | tr -d '\n\r' | awk '$1=$1' | sed 's/_/ /g' | grep -v "^$" | sort -u | head -n1)
	[ "$value" == "unknown" ] && value=""
	[ -n "$value" ] && [ -n "$2" ] && echo $(cut_str "$value" "$2") && return 0
	[ -n "$value" ] && echo "$value" && return 0
	return 1
}

# 获取文件最后修改时间（距离现在过去了多少秒）
file_date() {
	local file_dir="$1"
	local datetime=$(date -r "${file_dir}" +%s 2>/dev/null || echo "0")
	expr $(date +%s) - ${datetime}
}

# 流量数据单位换算
bytes_for_humans() {
	[ -z "$1" ] && return
	[ "$1" -gt 1073741824 ] && echo "$(awk 'BEGIN{printf "%.2f\n",'$1'/'1073741824'}') GB" && return
	[ "$1" -gt 1048576 ] && echo "$(awk 'BEGIN{printf "%.2f\n",'$1'/'1048576'}') MB" && return
	[ "$1" -gt 1024 ] && echo "$(awk 'BEGIN{printf "%.2f\n",'$1'/'1024'}') KB" && return
	echo "${1} bytes"
}

# 时间单位换算
time_for_humans() {
	[ -z "$1" ] && return

	if [ "$1" -lt 60 ]; then
		translate "%ds" "$1"
	elif [ "$1" -lt 3600 ]; then
		local minutes=$(( $1 / 60 ))
		local seconds=$(( $1 % 60 ))
		translate "%dm %ds" "$minutes" "$seconds"
	elif [ "$1" -lt 86400 ]; then
		local hours=$(( $1 / 3600 ))
		local minutes=$(( ($1 % 3600) / 60 ))
		translate "%dh %dm" "$hours" "$minutes"
	else
		local days=$(( $1 / 86400 ))
		local hours=$(( ($1 % 86400) / 3600 ))
		translate "%dd %dh" "$days" "$hours"
	fi
}

# 计算字符串显示宽度
length_str() {
	[ -z "$1" ] && return

	local result
	# 调试模式不要输出信息
	{
		local str="$1"
		local length=0

		while IFS= read -r -n1 char; do
			local char_width
			char_width=$(printf "%s" "$char" | awk '{
				if (match($0, /[一-龥]/)) print 2;
				else print 1;
			}')

			length=$((length + char_width))
		done <<< "$str"

		result="$length"
	} > /dev/null 2>&1

	echo "$result"
}

# 字符串显示宽度处理
cut_str() {
	[ -z "$1" ] && return
		[ -z "$2" ] && return
	local result
	# 调试模式不要输出信息
	{
		local str="$1"
		local max_width="$2"
		local current_width=0

		# 遍历字符串的每个字符
		for ((i = 0; i < ${#str}; i++)); do
			local char="${str:$i:1}"
			local char_width=$(length_str "$char")

			# 如果当前宽度加上当前字符的宽度超过最大宽度，则停止
			if [ $((current_width + char_width)) -gt "$max_width" ]; then
				break
			fi

			result="${result}${char}"
			current_width=$((current_width + char_width))
		done

		# 如果裁剪了字符串，则添加 ".."
		if [ "$current_width" -lt $(length_str "$str") ]; then
			result=$(echo "$result" | sed 's/ *$//')
			result="${result}.."
		fi
	} > /dev/null 2>&1

	echo "$result"
}

# 翻译
translate() {
	local template="$1"
	shift  # 移出第一个参数，剩余参数作为变量

	# 获取基础翻译

	local lua_script=$(cat <<LUA
	require "luci.i18n".setlanguage("$lang")
	print(require "luci.i18n".translate([==[$template]==]))
LUA
	)
	local translated=$(lua -e "$lua_script")

	# 如果有额外参数则进行格式化
	if [ $# -gt 0 ]; then
		printf "$translated" "$@"
	else
		echo "$translated"
	fi
}

# 随机数
rand() {
	local min=$1
	local max=$(($2 - $min + 1))
	local num=$(date +%s%N)
	echo $(($num % $max + $min))
}

# 日志输出函数，待设置调试等级
log_change() {
	local message="$1"
	echo "$(date "+%Y-%m-%d %H:%M:%S")" "${message}" >>"$logfile"
}

# 重启网络服务
network_restart() {
	echo '#!/bin/sh' >"${dir}/network_restart"
	echo '/etc/init.d/network restart >/dev/null 2>&1 &' >>"${dir}/network_restart"
	echo '/etc/init.d/firewall restart >/dev/null 2>&1 &' >>"${dir}/network_restart"
	echo '/etc/init.d/dnsmasq restart >/dev/null 2>&1 &' >>"${dir}/network_restart"

	chmod 0755 "${dir}/network_restart"
	"${dir}/network_restart"
	rm -f "${dir}/network_restart" >/dev/null 2>&1
}

# 文件锁
LockFile() {
	local fd=200

	if [ "$1" = "lock" ]; then
		eval "exec $fd>$lock_file"
		flock -n $fd
		if [ $? -ne 0 ]; then
			while ! flock -n $fd; do
				sleep 1
			done
		fi
	elif [ "$1" = "unlock" ]; then
		eval "exec $fd>&-"
	fi
}

# 检测退出信号
cleanup() {
	local pids=$(ps | grep -E "\{wechatpush\}_${MAIN_PID}|\{wechatpush-call\}" | grep -v grep | awk '{print $1}')
	[ -n "$pids" ] && echo "$pids" | xargs kill 2>/dev/null
	LockFile unlock
	$EXIT_FLAG && exit 0
}

# 子进程调用
run_with_tag() {
	[ -z "$1" ] && return
	local command_name=$1  # 第一个参数是命令名称
	shift # 移除第一个参数，剩下的参数传递给命令
	local command_path=$(readlink -f "$(which "$command_name")") # 检查命令路径

	# 如果是 BusyBox 的 applet，调用 wechatpush-call
	if [[ "$command_path" == *"busybox"* ]]; then
		/usr/libexec/wechatpush-call child "$command_name" "$@"
	else
		bash -c 'exec -a "$0" "$@"' "${PROCESS_TAG} ${command_name}" "$command_name" "$@"
	fi
}


# 清理临时文件
deltemp() {
	unset title content gateway_iplist
	rm -f "${dir}/title" "${dir}/content" "${dir}/send_enable.lock" "${tempjsonpath}" "${dir}/cookies.txt" >/dev/null 2>&1
	[ -f "$logfile" ] && local logrow=$(grep -c "" "$logfile") || local logrow="0"
	[ "$logrow" -gt 500 ] && tail -n 300 "$logfile" >"${logfile}.tmp" && mv "${logfile}.tmp" "$logfile" && log_change "[DEBUG] $(translate "Log exceeded limit, keeping last 300 entries")"
}

# ------------------------------------
# 信息获取类
#
# 获取 ip
getip() {
	[ -z "$1" ] && return

	# 从接口获取 IPv4
	if [ $1 == "wanipv4" ]; then
		[ -n "$ipv4_interface" ] && local wanIP=$(/sbin/ifconfig ${ipv4_interface} | awk '/inet addr/ {print $2}' | awk -F: '{print $2}' | grep -oE '[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}')
		[ -z "$ipv4_interface" ] && local wanIP=$(getinterfacelist | grep '\"address\"' | grep -oE '[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}')
		echo "$wanIP"
	# 从 URL 获取 IPv4
	elif [ $1 == "hostipv4" ]; then
		local sorted_ipv4_urllist=$(echo "$ipv4_urllist" | awk 'BEGIN {srand()} {print rand() "\t" $0}' | sort -k1,1n | cut -f2-)
		local ipv4_URL
		while IFS= read -r ipv4_URL; do
			[ -n "$ipv4_interface" ] && local tmp_hostIP=$(eval "curl --connect-timeout 2 -m 2 -k -s -4 --interface ${ipv4_interface} -m 5 ${ipv4_URL}") || local tmp_hostIP=$(eval "curl --connect-timeout 2 -m 2 -k -s -4 -m 5 ${ipv4_URL}")
			[ -z "$tmp_hostIP" ] && log_change "[DEBUG] $(translate "Failed to get IP, current API: %s & %s" "$ipv4_URL" "$ipv4_interface")"
			local tmp_hostIP=$(echo $tmp_hostIP | grep -oE '[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}' | head -n1)
			[ -n "$tmp_hostIP" ] && local tmp_hostIP="{\"IP\":\"${tmp_hostIP}\", \"URL\":\"${ipv4_URL}\"}" && break
		done <<<"$sorted_ipv4_urllist"
		echo $tmp_hostIP
	# 从接口获取 IPv6
	elif [ $1 == "wanipv6" ]; then
		[ -n "$ipv6_interface" ] && local wanIPv6=$(ip -6 addr show dev ${ipv6_interface} | grep "inet6" | grep "global dynamic noprefixroute" | awk '{print $2}' | grep -Ev "^(fc|fd)" | tail -n 1 | grep -oE "([\da-fA-F0-9]{1,4}(:{1,2})){1,15}[\da-fA-F0-9]{1,4}")
		[ -z "$ipv6_interface" ] && local wanIPv6=$(ip -6 addr show | grep "inet6" | grep "global dynamic noprefixroute" | awk '{print $2}' | grep -Ev "^(fc|fd)" | tail -n 1 | grep -oE "([\da-fA-F0-9]{1,4}(:{1,2})){1,15}[\da-fA-F0-9]{1,4}")
		echo "$wanIPv6"
	# 从 URL 获取 IPv6
	elif [ $1 == "hostipv6" ]; then
		local sorted_ipv6_urllist=$(echo "$ipv6_urllist" | awk 'BEGIN {srand()} {print rand() "\t" $0}' | sort -k1,1n | cut -f2-)
		local ipv6_URL
		while IFS= read -r ipv6_URL; do
			[ -n "$ipv6_interface" ] && local tmp_hostIPv6=$(eval "curl --connect-timeout 2 -m 2 -k -s -6 --interface ${ipv6_interface} -m 5 ${ipv6_URL}") || local tmp_hostIPv6=$(eval "curl --connect-timeout 2 -m 2 -k -s -6 -m 5 ${ipv6_URL}")
			[ -z "$tmp_hostIPv6" ] && log_change "[DEBUG] $(translate "Failed to get IPv6, current API: %s & %s" "$ipv6_URL" "$ipv6_interface")"
			local tmp_hostIPv6=$(echo $tmp_hostIPv6 | grep -oE "([\da-fA-F0-9]{1,4}(:{1,2})){1,15}[\da-fA-F0-9]{1,4}" | head -n1)
			[ -n "$tmp_hostIPv6" ] && local tmp_hostIP="{\"IP\":\"${tmp_hostIPv6}\", \"URL\":\"${ipv6_URL}\"}" && break
		done <<<"$sorted_ipv6_urllist"
		echo $tmp_hostIP
	fi
}

# 查询设备接口
getinterface() {
	local ip=${1}
	local mac=${2}
	local interface

	[ -z "$mac" ] && return
	[ "$mac" == "unknown" ] && return

	# 从已保存的地址中获取接口
	interface=$(jq -r --arg ip "$ip" --arg mac "$mac" '.devices[] | select(.ip == $ip and (.mac | ascii_downcase) == ($mac | ascii_downcase)) | .interface' "$devices_json")
	process_and_check "$interface" && return

	# 如果定义了 WLAN 接口列表，则查询每个接口
	if [ -n "$wlan_interface" ]; then
		for interface_wlan in $wlan_interface; do
			interface=$(iw dev "$interface_wlan" station dump 2>/dev/null | grep -i -w "$mac" | sed -nr 's#^.*on (.*))#\1#gp')
			process_and_check "$interface" && return
		done
	fi

	# 查询 ARP 表获取接口
	interface=$(cat /proc/net/arp | grep "0x2\|0x6" | grep -v "^169.254." | grep -i -w "$mac" | awk '{print $6}' | grep -v "^$" | sort -u)
	process_and_check "$interface" && return
}

# 查询 MAC 地址
getmac() {
	local ip="$1"
	local mac
	[ -z "$ip" ] && return 1

	# 已保存的 MAC
	mac=$(jq -r --arg ip "$ip" '.devices[] | select(.ip == $ip) | .mac' "$devices_json")
	# 某些路由器中继模式，会导致 MAC 重复，如果是重复值，尝试重新获取
	local mac_count=$(jq -r --arg mac "$mac" '.devices[] | select((.mac | ascii_downcase) == ($mac | ascii_downcase)) | .mac' "$devices_json" | wc -l)
	[ "$mac_count" -eq 1 ] && process_and_check "$mac" && return

	# DHCP
	[ -f "/tmp/dhcp.leases" ] && mac=$(grep -w "${ip}" "/tmp/dhcp.leases" | awk '{print $2}')
	process_and_check "$mac" && return

	# ARP
	mac=$(cat /proc/net/arp | grep "0x2\|0x6" | grep -w "${ip}" | awk '{print $4}')
	process_and_check "$mac" && return

	# 定时推送时不查询
	[ -f "${dir}/send_pid" ] && echo "unknown" && return

	# 离线、在线时间超过 10 分钟不查询
	local uptime=$(jq -r --arg ip "$ip" '.devices[] | select(.ip == $ip and .status == "online") | .uptime' "$devices_json")
	[ -n "$uptime" ] && local time_up=$(expr $(date +%s) - $uptime) && [ "$time_up" -lt 600 ] && mac=$(nmblookup_timeout "${ip}" "mac" "5" 2>/dev/null)
	process_and_check "$mac" && return

	# 默认返回 unknown
	echo "unknown"
}

# 查询主机名
getname() {
	local ip="$1"
	local mac="$2"
	local tmp_name
	local oui_name
	local name_width="20"
	[ -z "$ip" ] && return 1
	[ -z "$mac" ] && return 1

	# 自定义备注
	tmp_name=$(cat "$device_aliases_path" 2>/dev/null | grep -i -Ew "^${ip}|^${mac}" | awk '{for(i=2; i<=NF; i++) printf $i " "; print ""}')
	process_and_check "$tmp_name" "$name_width" && return

	# 光猫
	[ -f "${dir}/gateway_info" ] && {
		tmp_name=$(jq -r --arg ip "$ip" '.[] | select(.ip == $ip) | .name' "${dir}/gateway_info")
		process_and_check "$tmp_name" "$name_width" && return
	}

	# MikroTik
	[ -f "${dir}/mikrotik_info" ] && {
		tmp_name=$(jq -r --arg mac "$mac" '.[] | select((.mac | ascii_downcase) == ($mac | ascii_downcase)) | .name' "${dir}/mikrotik_info")
		process_and_check "$tmp_name" "$name_width" && return
	}

	# MIWiFi
	[ -f "${dir}/miwifi_info" ] && {
		tmp_name=$(jq -r --arg ip "$ip" '.[] | select(.ip == $ip) | .name' "${dir}/miwifi_info")
		process_and_check "$tmp_name" "$name_width" && return
	}

	# openwrt
	[ -f "${dir}/openwrt_info" ] && {
		tmp_name=$(jq -r --arg mac "$mac" '.[] | select((.mac | ascii_downcase) == ($mac | ascii_downcase)) | .name' "${dir}/openwrt_info")
		process_and_check "$tmp_name" "$name_width" && return
	}

	# 已保存的主机名
	tmp_name=$(jq -r --arg ip "$ip" --arg mac "$mac" '.devices[] | select(.ip == $ip and (.mac | ascii_downcase) == ($mac | ascii_downcase)) | .name' "$devices_json")
	[ -n "$oui_name" ] && [ -n "$tmp_name" ] && [ "$oui_name" == "$tmp_name" ] && tmp_name=""
	process_and_check "$tmp_name" "$name_width" && return

	# 静态地址备注名
	local dhcp_config=$(uci show dhcp | grep -i -w "${ip}\|${mac}" | sed -n 's/\(dhcp\..*\)\..*/\1/p')
	[ -n "$dhcp_config" ] && tmp_name=$(uci get ${dhcp_config}.name)
	process_and_check "$tmp_name" "$name_width" && return

	# DHCP
	[ -f "/tmp/dhcp.leases" ] && tmp_name=$(grep -w "${ip}" /tmp/dhcp.leases | awk '{print $4}')
	process_and_check "$tmp_name" "$name_width" && return

	# 定时推送时不查询
	[ -f "${dir}/send_pid" ] && echo "unknown" && return

	# MAC 获取失败时返回 unknown，优先级不能最高，自定义备注可以被 IP 匹配
	echo "$mac" | grep -q -w "unknown\|*" && echo "unknown" && return

	# 因 NetBIOS 查询时间过长，跳过离线设备
	# 设备刚上线时设备信息未保存，json 中无法检查在线状态，应避免使用 oui 数据库，否则无法使用 NetBIOS 查询
	# 离线、在线时间超过 10 分钟不查询
	local uptime=$(jq -r --arg ip "$ip" '.devices[] | select(.ip == $ip and .status == "online") | .uptime' "$devices_json")
	if [ -n "$uptime" ] && time_up=$(expr $(date +%s) - $uptime) && [ "$time_up" -lt 600 ]; then
		tmp_name=$(nmblookup_timeout "${ip}" "name" "5" 2>/dev/null)
		process_and_check "$tmp_name" "$name_width" && return

		# MAC 设备信息数据库
		[ -f "$oui_base" ] && oui_name=$(grep -i "$(echo "$mac" | cut -c 1,2,4,5,7,8)" "$oui_base" | sed -nr 's#^.*16)..(.*)#\1#gp')
		[ -n "$oui_data" ] && [ "$oui_data" -eq "4" ] && oui_name=$(curl -sS "https://standards-oui.ieee.org/oui/oui.txt" | grep -i "$(echo "$mac" | cut -c 1,2,4,5,7,8)" | sed -nr 's#^.*16)..(.*)#\1#gp')
		tmp_name="$oui_name"
		process_and_check "$tmp_name" "$name_width" && return
	fi

	echo "unknown"
}

# 获取接口信息
getinterfacelist() {
	[ $(ubus list | grep -w -i "network.interface.wan" | wc -l) -ge "1" ] && ubus call network.interface.wan status || ubus call network.interface.lan status && return
	[ -n "$ipv4_interface" ] && local device_name=$ipv4_interface || [ -n "$ipv6_interface" ] && local device_name=$ipv6_interface
	[ -n "$device_name" ] && local interface_name=$(ubus call network.interface dump | jq -r --arg intf "$device_name" '.interface[] | select(.device == $intf and (.interface | endswith("6") | not)) | .interface')
	[ -z "$interface_name" ] && local interface_name=$(ubus list | grep -i "network.interface." | grep -v "loopback" | grep -v -i "wan6" | grep -v -i "wan_6" | grep -v -i "lan6" | grep -v -i "ipsec.*" | grep -v -i "VPN.*" | grep -v -i "DOCKER.*" | awk -F '.' '{print $3}' | head -n1)
	ubus call network.interface.${interface_name} status && return
}

# 获取接口在线时间
getinterfaceuptime() {
	getinterfacelist | awk -F'\"uptime\": ' '/uptime/ { gsub(/,/, "", $2); print $2 }'
}

# 查询 IP 归属地
get_ip_attribution() {
	ip="$1"
	jq -e --arg ip "$ip" '.devices[] | select(.ip == $ip) | .ip' "$devices_json" >/dev/null && echo "本地局域网" && return
	local ip_attribution_urls=$(cat /usr/share/wechatpush/api/ip_attribution.list)
	local sorted_attribution_urls=$(echo "$ip_attribution_urls" | awk 'BEGIN {srand()} {print rand() "\t" $0}' | sort -k1,1n | cut -f2-)
	local ip_attribution_url
	while IFS= read -r ip_attribution_url; do
		local login_ip_attribution=$(eval curl --connect-timeout 2 -m 2 -k -s "$ip_attribution_url" 2>/dev/null)
		#logfile=logfile="${dir}/wechatpush.log" # 此处保留排查时使用，增加日志调试等级后再加入 ip_attribution_url
		#[ -z "$login_ip_attribution" ] && echo "$(date "+%Y-%m-%d %H:%M:%S") [DEBUG] $(translate "Location query timeout, current API: %s" "$ip_attribution_url")" >> "${logfile}"
		[ -n "$login_ip_attribution" ] && echo "$login_ip_attribution" && break
	done <<<"$sorted_attribution_urls"
}

# 查询 NetBIOS
nmblookup_timeout() {
	local ip="$1"
	local query_type="$2" # "name" or "mac"
	local max_wait_time="$3"
	local result=""
	[ -z "$ip" ] && return 1
	[ -z "$query_type" ] && return 1
	[ -z "$max_wait_time" ] && return 1

	{
		if [ "$query_type" == "name" ]; then
			result=$(run_with_tag nmblookup -A ${ip} 2>/dev/null | awk '/<00>/ && !/<GROUP>/ {print $1}')
		elif [ "$query_type" == "mac" ]; then
			result=$(run_with_tag nmblookup -A ${ip} 2>/dev/null | grep -oE 'MAC\s+Address\s+=\s+([0-9A-Fa-f]{2}[:-]){5}([0-9A-Fa-f]{2})' | awk '{print $NF}')
		fi
		echo "$result"
	} &
	local nmblookup_pid=$!

	# 等待最多 $max_wait_time 秒
	local wait_time=0
	while [ $wait_time -lt $max_wait_time ]; do
		# 检查子进程是否已完成
		if ! kill -0 $nmblookup_pid >/dev/null 2>&1; then
			result=$(wait $nmblookup_pid) # 获取子进程的输出
			break
		fi
		sleep 1
		wait_time=$((wait_time + 1))
	done

	if kill -0 $nmblookup_pid >/dev/null 2>&1; then
		kill $nmblookup_pid
	fi

	echo "$result"
}

# 查询 http/https 端口
check_http_access() {
	local ip="$1"
	[ -z "$ip" ] && return 1

	curl --head --silent --fail --connect-timeout 3 "http://$ip" >/dev/null
	[ $? -eq 0 ] && echo "http" && return

	curl --head --silent --fail --connect-timeout 3 "https://$ip" >/dev/null
	[ $? -eq 0 ] && echo "https" && return

	return 1
}

# ping
getping() {
	local ip="$1"
	local mac="$2"
	local timeout="$3"
	local retry_count="$4"

	local interface=$(getinterface "ip" "$mac")
	[ "$iw_version" ] && [ "$interface" ] && wlan_online=$(iw dev ${interface} station dump 2>/dev/null | grep -i -w "$mac" | grep Station) >/dev/null 2>&1
	[ "$wlan_online" ] && {
		silent_run LockFile lock
		jq --arg ip "$ip" --arg parent "Local" \
			'.devices |= map(if .ip == $ip and (.parent | . != $parent) then .parent = $parent else . end)' \
			"$devices_json" > "$tmp_devices_json" && mv "$tmp_devices_json" "$devices_json"
		silent_run LockFile unlock
		return 0
	}

	interface=$(cat /proc/net/arp | grep -w "$ip" | awk '{print $6}' | grep -v "^$" | sort -u | head -n1)
	for i in $(seq 1 "$retry_count"); do
		# arping 应使用 br-lan 等接口，有 WiFi 的设备接口为 wlan*，重新获取
		[ -n "$interface" ] && local arp_entry_count=$(cat /proc/net/arp | grep '0x[26]' | grep -i -w "$mac" | wc -l 2>/dev/null)

		# 避免无效的 arp 条目
		if [ -n "$arp_entry_count" ] && [ "$arp_entry_count" -le 1 ]; then
			ip_ms=$(run_with_tag arping -I "$interface" -c 20 -f -w "$timeout" "$ip" 2>/dev/null)
			echo "$ip_ms" | grep -q "ms" && break || ip_ms=$(run_with_tag ping -c 5 -w "$timeout" "$ip" 2>/dev/null | grep -v '100% packet loss')
		else
			ip_ms=$(run_with_tag ping -c 5 -w "$timeout" "$ip" 2>/dev/null | grep -v '100% packet loss')
			echo "$ip_ms" | grep -q "ms" && break || ip neigh del "$ip" dev "$interface" 2>/dev/null # 清理无效条目
		fi
		sleep 1
	done
	unset i
	echo "$ip_ms" | grep -q "ms"
}

# CPU 占用率
getcpu() {
	local AT=$(cat /proc/stat | grep "^cpu " | awk '{print $2+$3+$4+$5+$6+$7+$8 " " $2+$3+$4+$7+$8}')
	sleep 1
	local BT=$(cat /proc/stat | grep "^cpu " | awk '{print $2+$3+$4+$5+$6+$7+$8 " " $2+$3+$4+$7+$8}')
	local CPU_USAGE=$(echo ${AT} ${BT} | awk '{printf "%.01f%%", (($4-$2)/($3-$1))*100}')
	echo $CPU_USAGE
}

# 获取SOC温度 （取所有传感器温度最大值）
soc_temp() {
	[ -n "$soc_code" ] && [ "$soc_code" != "pve" ] && eval $(echo "$soc_code") 2>/dev/null && return 0
	getsensors() {
		# Intel
		local sensor_field1='["coretemp-isa-0000"]["Package id 0"]["temp1_input"]'
		# AMD
		local sensor_field2='["zenpower-pci-00c3"]["Tctl"]["temp1_input"]'
		local sensor_field3='["k10temp-pci-00c3"]["Tctl"]["temp1_input"]'
		run_with_tag ${1} sensors -j 2>/dev/null | jq -r "
			if .${sensor_field1} != null then
			  .${sensor_field1}
			elif .${sensor_field2} != null then
			  .${sensor_field2}
			elif .${sensor_field3} != null then
			  .${sensor_field3}
			else
			  null
			end // \"\"
		"
	}

	[ -n "$server_host" ] && local ssh_command="ssh -o ConnectTimeout=5 -o StrictHostKeyChecking=yes -o BatchMode=yes -i /root/.ssh/id_rsa root@${server_host} -p ${server_port}"

	local temperature=$(getsensors "$ssh_command" 2>/dev/null)
	# 通用（只能取最高温度，不一定是 CPU，特殊设备自行修改）
	# 将 grep °C 改为温度所在行的特别字符串，如 grep Core 0 等，就可以指定设备了
	[ -z "$temperature" ] && local temperature=$(sensors 2>/dev/null | grep °C | sed -nr 's#^.*:.*\+(.*)°C .*#\1#gp' | sort -nr | head -n1)
	# 将 thermal_zone* 改为 thermal_zone0 thermal_zone1 等，就可以指定设备了
	[ -z "$temperature" ] && local temperature=$(cat /sys/class/thermal/thermal_zone*/temp 2>/dev/null | sort -nr | head -n1 | cut -c-2)
	printf "%.1f" "$temperature"
}

# 流量数据
usage() {
	[ ! -f "/usr/sbin/wrtbwmon" ] || [ -z "$1" ] && return
	# 更新
	if [ $1 == "update" ]; then
		version_le() { test "$(echo "$@" | tr " " "\n" | sort -n | head -n 1)" == "$1"; }
		version_ge() { test "$(echo "$@" | tr " " "\n" | sort -r | head -n 1)" == "$1"; }
		[ -n "$wr_version" ] && (version_ge "${wr_version}" "1.2.0") && wrtbwmon -f ${dir}/usage.db 2>/dev/null && return
		[ -n "$wr_version" ] && (version_le "${wr_version}" "1.0.0") || [ -z "$wr_version" ] && wrtbwmon update ${dir}/usage.db 2>/dev/null && return
	# 获取
	elif [ $1 == "get" ]; then
		[ ! -f "${dir}/usage.db" ] && [ -z "$3" ] && echo $(bytes_for_humans "0") && return
		[ ! -f "${dir}/usage.db" ] && [ -n "$3" ] && echo "0" && return
		[ -z "$total_n" ] && total_n=$(cat ${dir}/usage.db | head -n1 | grep "total" | sed 's/,/\n/g' | awk '/total/{print NR}') 2>/dev/null
		[ -z "$total_n" ] && total_n="6"
		[ -n "$2" ] && local tmptotal=$(cat ${dir}/usage.db | sed 's/,,,/,0,0,/g' | sed 's/,,/,0,/g' | sed 's/,/ /g' | grep -i -w ${2} | awk "{print "'$'$total_n"}" | grep -v "^$" | sort -u | head -n1) 2>/dev/null
		[ -z "$tmptotal" ] && local tmptotal="0"
		[ -z "$3" ] && echo $(bytes_for_humans "${tmptotal}") || echo "$tmptotal"
	# 剔除
	elif [ $1 == "down" ]; then
		[ "$2" ] && sed -i "/,${2},/d" ${dir}/usage.db 2>/dev/null
	fi
}

# ------------------------------------
# 需要经常调用的偷懒类
#
# 检测程序开关
enable_detection() {
	[ -z "$1" ] && local time_n=1
	for i in $(seq 1 $time_n); do
		get_config enable
		[ -z "$enable" ] || [ "$enable" -eq "0" ] && exit || sleep 1
	done
	unset i
}

# 免打扰检测
disturb() {
	[ -z "$do_not_disturb_mode" ] || [ -z "$do_not_disturb_starttime" ] || [ -z "$do_not_disturb_endtime" ] && return 0

	# 非免打扰时间
	if [ $(date +%H) -ge $do_not_disturb_endtime -a $do_not_disturb_starttime -lt $do_not_disturb_endtime ] || [ $(date +%H) -lt $do_not_disturb_starttime -a $do_not_disturb_starttime -lt $do_not_disturb_endtime ] || [ $(date +%H) -lt $do_not_disturb_starttime -a $(date +%H) -ge $do_not_disturb_endtime -a $do_not_disturb_starttime -gt $do_not_disturb_endtime ]; then
		unset sheep_starttime
		rm -f ${dir}/sheep_usage ${dir}/old_sheep_usage 2>/dev/null
		[ -z "$jsonpath" ] && disturb_text="[INFO]"
		[ -n "$jsonpath" ] && disturb_text=$(jq -r '._api' ${jsonpath})
		return 0
	# 免打扰
	else
		[ -z "$sheep_starttime" ] && log_change "[DND] $(translate "It's late at night, time to rest")" && sheep_starttime=$(date +%s)
		# 挂起
		if [ "$do_not_disturb_mode" -eq "1" ]; then
			while [ $(date +%H) -lt "$do_not_disturb_endtime" ]; do
				enable_detection
				sleep $sleeptime
			done
		# 静默
		elif [ "$do_not_disturb_mode" -eq "2" ]; then
			disturb_text="[DND]"
			return 1
		fi
	fi
}

# 检测黑白名单
blackwhitelist() {
	local mac="$1"
	[ -z "$mac" ] && return 1

	# return 1 免打扰
	# return 0 正常推送

	# 没有打开免打扰功能
	[ -z "$up_down_push_whitelist" ] && [ -z "$up_down_push_blacklist" ] && [ -z "$up_down_push_interface" ] && [ -z "$mac_online_list" ] && [ -z "$mac_offline_list" ] && return 0

	# 忽略列表内设备
	[ -n "$up_down_push_whitelist" ] && (echo "$up_down_push_whitelist" | grep -q -i -w "$mac") && return 1
	# 仅通知列表内设备
	[ -n "$up_down_push_blacklist" ] && (! echo "$up_down_push_blacklist" | grep -q -i -w "$mac") && return 1
	# 仅通知接口
	[ -n "$up_down_push_interface" ] && (! echo $(getinterface "" "$mac") | grep -q -i -w $up_down_push_interface) && return 1

	for check_mac in $mark_mac_list; do
		# 设备在线时免打扰
		[ -n "$mac_online_list" ] && jq -e --arg mac "$check_mac" '.devices[] | select((.mac | ascii_downcase) == ($mac | ascii_downcase) and .status == "online") | .mac' "$devices_json" >/dev/null && return 1
		# 设备离线时免打扰
		[ -n "$mac_offline_list" ] && jq -e --arg mac "$check_mac" '.devices[] | select((.mac | ascii_downcase) == ($mac | ascii_downcase) and .status == "offline") | .mac' "$devices_json" >/dev/null && return 1
	done

	unset check_mac
	return 0
}

# 查看无人值守任务设备是否在线
geterrdevicealiases() {
	[ -z "$unattended_device_aliases" ] && return
	local logrow=$(jq '.devices | map(select(.status == "online")) | length' "$devices_json")
	[ "$logrow" -eq 0 ] && return

	for mac in $unattended_device_aliases; do
		[ -n "$mac" ] && local unattended_mac=$(jq -r --arg mac "$mac" '.devices[] | select((.mac | ascii_downcase) == ($mac | ascii_downcase) and .status == "online") | .mac' "$devices_json") && break
	done

	# 进入免打扰时间已经超过一小时
	if [ -n "$sheep_starttime" ] && [ "$(($(date +%s) - $sheep_starttime))" -ge "3600" ]; then
		>${dir}/sheep_usage
		local MACLIST=$(echo "$unattended_device_aliases" | grep -v "^$" | sort -u)
		while IFS= read -r mac; do
			[ -n "$mac" ] && local tmptotal=$(usage get ${mac} bytes)
			[ -n "$tmptotal" ] && awk 'BEGIN{printf "%.0f\n",'$tmptotal'/'204800'}' 2>/dev/null >>${dir}/sheep_usage
		done <<<"$MACLIST"
		[ -f ${dir}/old_sheep_usage ] && local old_sheep_usage=$(cat ${dir}/old_sheep_usage) 2>/dev/null || local old_sheep_usage=""
		[ -f ${dir}/sheep_usage ] && local sheep_usage=$(cat ${dir}/sheep_usage) 2>/dev/null || local sheep_usage=""
		[ "$old_sheep_usage" == "$sheep_usage" ] && [ -z "$sheep_nousage_starttime" ] && sheep_nousage_starttime=$(date +%s)
		[ "$old_sheep_usage" != "$sheep_usage" ] && unset sheep_nousage_starttime && cat ${dir}/sheep_usage 2>/dev/null >${dir}/old_sheep_usage
		[ -n "$sheep_nousage_starttime" ] && [ "$(($(date +%s) - $sheep_nousage_starttime))" -ge "300" ] && unset unattended_mac
	fi
	[ -z "$unattended_mac" ]
}

# ------------------------------------
# 网络状态相关
#
# 检测 ip 状况
ip_changes() {
	local IPv4_URL="$(translate "Network Interface")"
	local IPv6_URL="$(translate "Network Interface")"
	[ "$get_ipv4_mode" -eq "1" ] && getip wanipv4 >"$output_dir/IPv4" &
	[ "$get_ipv6_mode" -eq "1" ] && getip wanipv6 >"$output_dir/IPv6" &
	[ "$get_ipv4_mode" -eq "2" ] && local IPv4=$(getip hostipv4) && local IPv4_URL=$(echo ${IPv4} | jq -r '.URL') && local IPv4=$(echo ${IPv4} | jq -r '.IP')
	[ "$get_ipv6_mode" -eq "2" ] && local IPv6=$(getip hostipv6) && local IPv6_URL=$(echo ${IPv6} | jq -r '.URL') && local IPv6=$(echo ${IPv6} | jq -r '.IP')
	wait_and_cat

	if [ "$1" ] && [ "$1" == "getip" ]; then
		echo "IPv4: ${IPv4}<br />\
$(translate 'Location:') $(get_ip_attribution ${IPv4})<br />\
$(translate 'Interface:') ${IPv4_URL}<br />\
IPv6: ${IPv6}<br />\
$(translate 'Location:') $(get_ip_attribution ${IPv6})<br />\
$(translate 'Interface:') ${IPv6_URL}"
		return
	fi

	local last_IPv4=$(jq -r '.address[0].IPv4 // empty' "$devices_json")
	local last_IPv6=$(jq -r '.address[0].IPv6 // empty' "$devices_json")
	silent_run LockFile lock

	if [ -z "$last_IPv4" ] && [ -z "$last_IPv6" ] && [[ -n "$IPv4" || -n "$IPv6" ]]; then
		log_change "${disturb_text} $(translate "Router has rebooted!")"
		title="$(translate "Router Rebooted")"
		content="${content}\
${str_splitline}\
${str_title_start}$(translate "Router Rebooted")${str_title_end}"
	fi

	if [ "$get_ipv4_mode" -ne "0" ] && [ -n "$IPv4" ] && [ "$IPv4" != "$last_IPv4" ]; then
		log_change "${disturb_text} $(translate "Current IP: %s from: %s" "$IPv4" "$IPv4_URL")"
		[ -z "$title" ] && title="$(translate "IP Address Changed")"
		[ -z "$content" ] && content="${content}\
${str_splitline}\
${str_title_start}$(translate "IP Address Changed")${str_title_end}"
		content="${content}\
${str_linefeed}${str_tab} - $(translate "Current IP:")  ${IPv4}"
		jq --arg IPv4 "$IPv4" '.address[0].IPv4 = $IPv4' "$devices_json" >"$tmp_devices_json" && mv "$tmp_devices_json" "$devices_json"
	fi

	if [ "$get_ipv6_mode" -ne "0" ] && [ -n "$IPv6" ] && [ "$IPv6" != "$last_IPv6" ]; then
		log_change "${disturb_text} $(translate "Current IPv6: %s from: %s" "$IPv6" "$IPv6_URL")"
		[ -z "$title" ] && title="$(translate "IPv6 Address Changed")"
		[ -z "$content" ] && content="${content}\
${str_splitline}\
${str_title_start}$(translate "IPv6 Address Changed")${str_title_end}"
		content="${content}\
${str_linefeed}${str_tab} - $(translate "Current IPv6:") ${IPv6}"
		jq --arg IPv6 "$IPv6" '.address[0].IPv6 = $IPv6' "$devices_json" >"$tmp_devices_json" && mv "$tmp_devices_json" "$devices_json"
	fi

	# IP 变化，悄咪咪的重启 zerotier
	if [ -n "$content" ] && [ -n "$zerotier_helper" ] && [ "$zerotier_helper" -eq "1" ]; then
		[ -z "$zerotier_enabled" ] && zerotier_enabled=$(uci get zerotier.sample_config.enabled)
		if [ -n "$zerotier_enabled" ] && [ $zerotier_enabled -eq "1" ]; then
			/etc/init.d/zerotier restart >/dev/null 2>&1
		fi
	fi
	silent_run LockFile unlock
}

# 检测网络状态
check_connect() {
	# 获取网络状态
	is_online() {
		local urls=(http://connect.rom.miui.com/generate_204 http://wifi.vivo.com.cn/generate_204 http://connectivitycheck.platform.hicloud.com/generate_204 http://www.apple.com/library/test/success.html)
		local shuffled_urls=($(for i in "${urls[@]}"; do echo "$i"; done | awk 'BEGIN {srand()} {print rand(), $0}' | sort -n | cut -d' ' -f2-))
		for url in "${shuffled_urls[@]}"; do
			local status_code
			status_code=$(curl -o /dev/null -sI -w "%{http_code}" "$url")
			[[ "$status_code" -eq 204 || "$status_code" -eq 200 ]] && return 0
		done
		unset i url
		return 1
	}

	local network_state="unknown"
	while true; do
		if is_online; then
			[ "$network_state" == "down" ] && log_change "[INFO] $(translate "Network connection restored")"
			break
		else
			[ "$network_state" == "unknown" ] && log_change "[ERROR] $(translate "Network connection failed! Stopping detection!")"
			local network_state="down"
			# 无人值守、待弃用或改进
			[ -z "$network_unattended_time" ] && network_unattended_time=$(date +%s)
			if [ -n "$network_disconnect_event" ] && [ "$(($(date +%s) - $network_unattended_time))" -ge "300" ]; then
				>"${dir}/send_enable.lock" && first && deltemp
				geterrdevicealiases
				if [ "$?" -eq "0" ]; then
					[ -f /usr/share/wechatpush/autoreboot_count ] && retry_count=$(cat /usr/share/wechatpush/autoreboot_count) && rm -f /usr/share/wechatpush/autoreboot_count >/dev/null 2>&1
					[ -z "${retry_count}" ] && retry_count=0
					retry_count=$((retry_count + 1))
					if [ "$network_disconnect_event" -eq "1" ]; then
						if [ "$retry_count" -lt "3" ]; then
							echo "$retry_count" >/usr/share/wechatpush/autoreboot_count
							log_change "[WARN] $(translate "Attempting router reboot (attempt %s/3)" "$retry_count")"
							cat ${logfile} >/usr/share/wechatpush/errlog
							getgateway "reboot"
							sleep 2 && reboot && exit
						fi
						[ "$retry_count" -eq "3" ] && log_change "[ERROR] $(translate "Failed after 2 router reboots, please fix manually")"
					elif [ "$network_disconnect_event" -eq "2" ]; then
						[ "$retry_count" -lt "3" ] && log_change "[WARN] $(translate "Attempting network restart (attempt %s/3)" "$retry_count")"
						[ "$retry_count" -eq "3" ] && log_change "[ERROR] $(translate "Failed after 2 network restarts, please fix manually")"
					fi
				fi
			elif [ -f /usr/share/wechatpush/autoreboot_count ]; then
				network_unattended_time=$((network_unattended_time - 300)) && sleep 60
			fi
			enable_detection
			sleep $sleeptime
		fi
	done
	rm -f /usr/share/wechatpush/autoreboot_count >/dev/null 2>&1
}

# ------------------------------------
# 运行状态相关
#
# 检测 cpu 状态
check_cpu_load() {
	[ -z "$cpu_notification_duration" ] && cpu_notification_duration=$(time_for_humans $cpu_notification_delay)
	if [ -n "$notification_temp" ] && [ -n "$temperature_threshold" ]; then
		[ -z "$temp_last_overload_time" ] && temp_last_overload_time=$(date +%s)
		local cpu_temp=$(soc_temp)

		if [ -n "$cpu_temp" ] && awk -v t="$cpu_temp" -v th="$temperature_threshold" 'BEGIN { exit !(t > th) }'; then
			log_change "[WARN] $(translate "CPU temperature too high: %s" "$cpu_temp")"

			# 检查是否达到持续时间阈值
			if [ "$cpu_threshold_duration" -eq 0 ] || [ "$(($(date +%s) - $temp_last_overload_time))" -ge "$cpu_threshold_duration" ]; then
				# 检查是否达到推送间隔阈值
				if [ -z "$temperaturecd_time" ] || [ "$(($(date +%s) - $temperaturecd_time))" -ge "$cpu_notification_delay" ]; then
					if [ -n "$title" ] && (echo "$title" | grep -q "$(translate "too high")"); then
						title="$(translate "Device Alert!")"
					else
						title="$(translate "CPU Temperature Too High!")"
					fi

					temperaturecd_time=$(date +%s) # 记录本次推送的时间戳
					log_change "[WARN] $(translate "CPU temperature too high: %s" "$cpu_temp")"

					local cpu_overload_duration=$(time_for_humans $(($(date +%s) - $temp_last_overload_time)))
					local content="${content}\
${str_splitline}\
${str_title_start}$(translate "CPU Temperature Alert")${str_title_end}\
${str_linefeed}${str_tab} - $(translate "CPU temperature has exceeded threshold for %s" "$cpu_overload_duration")\
${str_linefeed}${str_tab} - $(translate "No further alerts for %s" "$cpu_notification_duration")\
${str_linefeed}${str_tab} - $(translate "Current temperature: %s℃" "$cpu_temp")"
				fi
			fi
		else
			# 温度正常，重置计时器
			temp_last_overload_time=$(date +%s)
		fi
	fi

	if [ -n "$notification_load" ] && [ -n "$cpu_load_threshold" ]; then
		[ -z "$cpu_last_overload_time" ] && cpu_last_overload_time=$(date +%s)
		local cpu_load=$(cat /proc/loadavg | awk '{print $1}') 2>/dev/null

		if [ -n "$cpu_load" ] && awk -v t="$cpu_load" -v th="$cpu_load_threshold" 'BEGIN { exit !(t > th) }'; then
			log_change "[ALERT] $(translate "CPU load too high: ${cpu_load}")"
			cputop log

			# 检查是否达到持续时间阈值
			if [ "$cpu_threshold_duration" -eq 0 ] || [ "$(($(date +%s) - $cpu_last_overload_time))" -ge "$cpu_threshold_duration" ]; then
				# 检查是否达到推送间隔阈值
				if [ -z "$cpucd_time" ] || [ "$(($(date +%s) - $cpucd_time))" -ge "$cpu_notification_delay" ]; then
					if [ -n "$title" ] && (echo "$title" | grep -q "$(translate "too high")"); then
						title="$(translate "Device Alert!")"
					else
						title="$(translate "CPU Load Too High!")"
					fi

					cpucd_time=$(date +%s) # 记录本次推送的时间戳
					log_change "${disturb_text} $(translate "CPU load too high: %s" "${cpu_load}")"

					local cpu_load_duration=$(time_for_humans $(($(date +%s) - $cpu_last_overload_time)))
					local content="${content}\
${str_splitline}\
${str_title_start}$(translate "CPU Load Alert")${str_title_end}\
${str_linefeed}${str_tab} - $(translate "CPU load has exceeded threshold for %s" "$cpu_load_duration")\
${str_linefeed}${str_tab} - $(translate "No further alerts for %s" "$cpu_notification_duration")\
${str_linefeed}${str_tab} - $(translate "Current load: %s" "$cpu_load")"

					cputop
				fi
			fi
		else
			# 负载正常，重置计时器
			cpu_last_overload_time=$(date +%s)
		fi
	fi
}

# CPU 占用前三
cputop() {
	[ -z "$1" ] && content="${content}\
	${str_splitline}\
	${str_title_start}$(translate "Top 3 CPU Processes")${str_title_end}"
	local gettop=$(top -bn 1 | grep -v "top -bn 1" | head -n 7)
	for i in $(seq 5 7); do
		local top_name=$(echo "${gettop}" | awk 'NR=='${i} | awk '{print ($8 ~ /\/bin\/sh|\/bin\/bash/) ? $9 : $8}')
		local top_load=$(echo "${gettop}" | awk 'NR=='${i} | awk '{print $7}')
		local temp_top="${top_name} ${top_load}"
		[ -n "$1" ] && local logtop="$logtop  $temp_top"
		[ -z "$1" ] && content="${content}\
		${str_linefeed}${str_tab} - ${temp_top}"
	done
	unset i
	[ -n "$1" ] && log_change "[ALERT] $(translate "Top 3 CPU processes: %s" "$logtop")"
}

# 检测硬盘状态
get_disk() {
	mkdir -p "${dir}/disk_info" && >"$output_dir/get_disk"

	# 获取磁盘名称
	get_disk_names() {
		local disk_names=($(lsblk -dno NAME,TYPE 2>/dev/null | awk '$2=="disk" && $1 !~ /^mtd/ && $1 !~ /^ubiblock/ {print $1}' | sort -u))
		[ -z "$disk_names" ] && disk_names=($(df -h 2>/dev/null | awk '$1 ~ /^\/dev/ && !/^\/dev\/(mtd|ubiblock)/ {if ($NF ~ /^\/[a-zA-Z0-9]/) { sub("/dev/", "", $1); sub(/[0-9]+$/, "", $1); print $1 }}' | sort -u))
		echo "${disk_names[@]}"
	}

	# 查询本地硬盘名
	local local_disk_names=($(get_disk_names))

	# 查询远程硬盘名
	if [ -n "$server_host" ]; then
		local ssh_command="ssh -o ConnectTimeout=5 -o StrictHostKeyChecking=yes -o BatchMode=yes -i /root/.ssh/id_rsa root@${server_host} -p ${server_port}"
		local remote_disk_names=$(run_with_tag $ssh_command "$(declare -f get_disk_names); get_disk_names" | tr -d '\r')
		local remote_disk_names=($remote_disk_names)
		local remote_disk_tags=($(for _ in "${remote_disk_names[@]}"; do echo "remote"; done))
		# 删除重复的直通硬盘
		local get_host_disk_uuids=$(run_with_tag $ssh_command ls -l /dev/disk/by-uuid/ | awk '{print $9}')
		for uuid in $get_host_disk_uuids; do
			local fstab_config=$(uci show fstab | grep -i -w "${uuid}" | sed -n 's/\(fstab\..*\)\..*/\1/p')
			[ -n "$fstab_config" ] && local tmp_target=$(uci get ${fstab_config}.target)
			[ -n "$tmp_target" ] && local local_dev=$(df -h 2>/dev/null | grep -w "${tmp_target}" | awk '$1 ~ /^\/dev/ && !/^\/dev\/(mtd|ubiblock)/ {if ($NF ~ /^\/[a-zA-Z0-9]/) { sub("/dev/", "", $1); sub(/[0-9]+$/, "", $1); print $1 }}' | sort -u)
			[ -n "$local_dev" ] && local local_disk_names=(${local_disk_names[@]/$local_dev/})
		done
		unset uuid
	fi

	# 先删除重复的直通硬盘，再添加标记
	local local_disk_tags=($(for _ in "${local_disk_names[@]}"; do echo "local"; done))

	# 合并本地和远程硬盘名及标记
	local all_disk_names=("${local_disk_names[@]}" "${remote_disk_names[@]}")
	local all_disk_tags=("${local_disk_tags[@]}" "${remote_disk_tags[@]}")

	for i in "${!all_disk_names[@]}"; do
		local tmp_name=${all_disk_names[i]}
		local tmp_tag=${all_disk_tags[i]}
		local error_pattern="No such device|Unable to detect device type|Unknown USB bridge|QEMU HARDDISK"

		# 判断硬盘类型
		if [ "$tmp_tag" == "remote" ]; then
			local disk_type="_remote"
			local tmp_command="$ssh_command"
		fi
		local file_path="${dir}/disk_info/${tmp_name}${disk_type}"

		# 如果不能获取值，使用分区名重试（因为不清楚是 OpenWrt 的原因还是 smartctl 版本的原因，使用出错重试的方式） // 2024/07/10 - 未重新验证
		run_with_tag ${tmp_command} smartctl -i -n standby "/dev/${tmp_name}" 2>/dev/null | grep -qE "$error_pattern" && {
			local tmp_name=$(run_with_tag ${tmp_command} df -h | awk "/^\\/dev\\/${tmp_name}/ {print \$1}" | awk -F '/' '{print $NF}' | head -n1)
			[ -z "$tmp_name" ] && continue
		}

		# 手上的硬盘不能休眠，不确定命令是否会唤醒硬盘，每天只运行一次
		last_disk_time=$(date -r "${file_path}" +%s 2>/dev/null) || last_disk_time=0

		if [ $(($(date +%s) - $last_disk_time)) -gt 86000 ]; then
			local disk_info=$(run_with_tag ${tmp_command} smartctl -i -n standby "/dev/${tmp_name}" 2>/dev/null)
			[ -z "$disk_info" ] && {
				cat /sys/block/"${tmp_name}"/device/model 2>/dev/null | grep -qE "$error_pattern" && continue
			} ||
				echo "$disk_info" | grep -qE "$error_pattern" && {
				continue
			} ||
				echo "$disk_info" | grep -q "STANDBY" && {
				echo "$disk_info" >"${file_path}"
			} ||
				run_with_tag ${tmp_command} smartctl -a -j /dev/${tmp_name} 2>/dev/null >${file_path}
		fi

		# 硬盘状态
		if [ -f "${file_path}" ] && [ -s "${file_path}" ] && (! cat "${file_path}" | grep -q -v "STANDBY"); then
			local disk_name=$(awk '/Device Model/{print $NF}' "$file_path")
			[[ -n $disk_name && $disk_name != null && $disk_name != 0 ]] && {
				local disk_name=$(cut_str "$disk_name" "20")
				local disk_name="${disk_name}_$(run_with_tag ${tmp_command} lsblk -o NAME,SIZE | awk "/^${tmp_name}/ {print \$NF}")"
			}
			echo -n "${str_linefeed}${str_title_start}$(translate "Disk Name:") ${disk_name}${disk_type}${str_title_end}${str_linefeed}${str_tab} - $(translate "Disk in standby")" >>"$output_dir/get_disk"
		elif [ -f "${file_path}" ]; then
			# 硬盘名称
			local disk_name=$(jq -r .model_name ${file_path})
			[ -z "$disk_name" ] && local disk_name=$(cat /sys/block/"${tmp_name}"/device/model)
			[[ -n $disk_name && $disk_name != null && $disk_name != 0 ]] && {
				local disk_name=$(cut_str "$disk_name" "20")
				local disk_size=$(run_with_tag ${tmp_command} lsblk -o NAME,SIZE 2>/dev/null | awk "/^${tmp_name}/ {print \$NF}")
				[ -z "$disk_size" ] && disk_size=$(run_with_tag ${tmp_command} df -h 2>/dev/null | awk -v tmp_disk_name="${tmp_name}" '$1 ~ "^/dev/"tmp_disk_name && !disk_found {print $2; disk_found=1}')
				local disk_name="${disk_name}_${disk_size}"
			}
			echo -n "${str_linefeed}${str_title_start}$(translate "Disk Name:") ${disk_name}${disk_type}${str_title_end}" >>"$output_dir/get_disk"

			# 硬盘温度
			local disk_temp=$(jq -r .temperature.current ${file_path})
			[[ -n $disk_temp && $disk_temp != null && $disk_temp != 0 ]] && echo -n "${str_linefeed}${str_tab} - $(translate "Disk Temp:") ${disk_temp}℃" >>"$output_dir/get_disk"

			# 通电时间
			local disk_time=$(jq -r .power_on_time.hours ${file_path})
			[[ -n $disk_time && $disk_time != null ]] && echo -n "${str_linefeed}${str_tab} - $(translate "Power On:") ${disk_time}h" >>"$output_dir/get_disk"

			# 空间使用
			local disk_use=$(run_with_tag ${tmp_command} lsblk -no NAME,FSUSE% "/dev/${tmp_name}" 2>/dev/null | awk '$2 != "" { gsub(/^[^a-zA-Z0-9]+/, "", $1); printf "%s: %s\n", $1, $2 }')
			[ -z "$disk_use" ] && disk_use=$(run_with_tag ${tmp_command} df -h 2>/dev/null | awk -v part_name="${tmp_name}" '$1 ~ "^/dev/"part_name && NF > 1 {sub("/dev/", "", $1); if (!seen[$1]) { printf "%s: %s  ", $1, $5; seen[$1] = 1; }} END {print ""}')
			[ -z "$disk_use" ] && [ -n "$tmp_command" ] && {
				local uuid=$(run_with_tag $tmp_command ls -l /dev/disk/by-uuid/ 2>/dev/null | grep "$disk" | awk '{print $9}')
				local fstab_config=$(uci show fstab 2>/dev/null | grep -i -w "${uuid}" | sed -n 's/\(fstab\..*\)\..*/\1/p')
				[ -n "$fstab_config" ] && local mount_point=$(uci get ${fstab_config}.target 2>/dev/null)
				[ -n "$mount_point" ] && local disk_use=$(df -h 2>/dev/null | awk -v mount="$mount_point" '$NF == mount {sub("^/dev/", "", $1); printf "%s: %s\n", $1, $5}')
			}
			[ -n "$disk_use" ] && [ $(echo "$disk_use" | wc -l) -ne "1" ] && disk_use=$(echo "$disk_use" | grep -v -w "0%")
			[ -n "$disk_use" ] && disk_use=$(echo "$disk_use" | sed ':a;N;$!ba;s/\n/'"${str_linefeed}${str_tab} - ${str_space}${str_space}${str_space}${str_space}${str_space}${str_space}${str_space}${str_space}${str_space}"'/g')
			[ -n "$disk_use" ] && [ -n "${disk_use// /}" ] && echo -e -n "${str_linefeed}${str_tab} - $(translate "Disk Usage:") ${disk_use}" >>"$output_dir/get_disk"

			# 寿命使用
			local disk_health=$(jq -r .nvme_smart_health_information_log.percentage_used ${file_path})
			[[ -n $disk_health && $disk_health != null ]] && echo -e -n "${str_linefeed}${str_tab} - $(translate "Health Used:") ${disk_health}%" >>"$output_dir/get_disk"

			# 错误日志
			local disk_log_err=$(jq -r .ata_smart_error_log.summary.count ${file_path})
			[[ -n $disk_log_err && $disk_log_err != null && $disk_log_err != 0 ]] && local disk_err="true" && echo -n "${str_linefeed}${str_tab} - $(translate "Error Logs:") ${disk_log_err}" >>"$output_dir/get_disk"

			# 自检错误
			local disk_test_err=$(jq -r .ata_smart_self_test_log.standard.error_count_total ${file_path})
			[[ -n $disk_test_err && $disk_test_err != null && $disk_test_err != 0 ]] && local disk_err="true" && echo -n "${str_linefeed}${str_tab} - $(translate "Self-Test Errors:") ${disk_test_err}" >>"$output_dir/get_disk"

			# 0E 错误
			local disk_0e_err=$(jq -r .nvme_smart_health_information_log.media_errors ${file_path})
			[[ -n $disk_0e_err && $disk_0e_err != null && $disk_0e_err != 0 ]] && local disk_err="true" && echo -n "${str_linefeed}${str_tab} - $(translate "0E Errors:") ${disk_0e_err}" >>"$output_dir/get_disk"

			# 整体健康
			local smart_status=$(jq -r .smart_status.passed ${file_path})
			[[ -n $smart_status && $smart_status != null && $smart_status != "true" ]] && {
				echo -e -n "${str_linefeed}${str_tab} - ${str_title_start}$(translate "Disk health assessment FAILED!!!")${str_title_end}" >>"$output_dir/get_disk"
				local disk_err="true"
			}
			[ -n "$disk_err" ] && echo -e -n "${str_linefeed}${str_tab} - ${str_title_start}$(translate "Disk has errors, please backup data immediately!!!")${str_title_end}" >>"$output_dir/get_disk"
		fi
	done
	unset i
}

# ------------------------------------
# 设备在线状态相关
#
# 扫描范围内 IP
scanlocalip() {
	[ -z "$scan_ip_range" ] && return
	[ -z "$last_scan_ip_time" ] && last_scan_ip_time=0

	local current_time=$(date +%s)
	local elapsed_time=$((current_time - last_scan_ip_time))

	# 判断是否需要重新扫描
	if [ "$elapsed_time" -ge "$device_info_helper_sleeptime" ]; then
		local start_ip=$(echo "$scan_ip_range" | cut -d "-" -f 1)
		local end_ip=$(echo "$scan_ip_range" | cut -d "-" -f 2)

		local start_num=$(echo "$start_ip" | awk -F '.' '{print $NF}')
		local end_num=$(echo "$end_ip" | awk -F '.' '{print $NF}')
		local base_ip=$(echo "$start_ip" | awk -F '.' '{print $1"."$2"."$3}')

		tmp_thread_num=5
		# 临时增加并发数
		for i in $(seq 1 "$tmp_thread_num"); do
			echo >&5
		done
		unset i

		for i in $(seq "$start_num" "$end_num"); do
			# 获取一个令牌
			read -u 5
			{
				run_with_tag ping -c 1 -W 2 "${base_ip}.${i}" >/dev/null 2>&1
				# 释放令牌
				echo >&5
			} &
		done
		unset i
		wait

		# 减少临时并发数
		for i in $(seq 1 "$tmp_thread_num"); do
			read -u 5
		done
		unset i

		last_scan_ip_time="$current_time"
	fi
}

# 从光猫处获取设备信息
getgateway() {
	[ -z "$gateway_info_enable" ] && return
	[ "$1" ] && [ "$1" == "reboot" ] && last_getgateway_time="$device_info_helper_sleeptime" || last_getgateway_time=$(file_date "${dir}/gateway_info")

	if [ "$last_getgateway_time" -ge "$device_info_helper_sleeptime" ]; then
		# 登录
		local loginfo=$(run_with_tag curl -s -L "${gateway_host_url}" -c ${dir}/cookies.txt -d "${gateway_username_id}=${gateway_username}&${gateway_password_id}=${gateway_password}") 2>/dev/null
		[ -n "$loginfo" ] && local mytoken=$(echo $loginfo | sed 's/{/\n/g' | grep token | awk '/realRestart/{print $2}' | sed $'s/\'//g')
		# 获取
		[ -n "$mytoken" ] && local get_gateway=$(run_with_tag curl -s -b ${dir}/cookies.txt "${gateway_info_url}" -d 'token='$mytoken)
		# 重启
		[ "$1" ] && [ "$1" == "reboot" ] && run_with_tag curl -s -b ${dir}/cookies.txt "${gateway_host_url}/admin/reboot" -d "token=$mytoken" >/dev/null 2>&1
		# 注销
		[ -n "$get_gateway" ] && [ -n "$gateway_logout_url" ] && run_with_tag curl -s -b ${dir}/cookies.txt "${gateway_logout_url}" -d 'token='$mytoken 2>/dev/null
		[ -z "$get_gateway" ] && log_change "[WARN] $(translate "Failed to get modem information, maybe current user not logged out or wrong settings")"

		local gateway_host_ip=$(echo "$gateway_host_url" | grep -oE '[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}')
		# 保存信息
		echo "$get_gateway" | jq --arg gateway_host_ip "$gateway_host_ip" '
			to_entries | map(select(.value | type == "object" and has("ip"))) | map({
				ip: .value.ip,
				name: (.value.devName // "unknown"),
				mac: "unknown",
				type: (if .key | startswith("wifi") then "WiFi" else "LAN" end),
				parent: (if .key | startswith("wifi") then $gateway_host_ip else "" end)
			})
		' > "${dir}/gateway_info"

		gateway_iplist=$(jq -r '.[] | select(.ip != "unknown" and .ip != "") | .ip' "${dir}/gateway_info")
	else
		unset gateway_iplist
	fi
}

# 从小米路由器获取设备信息
getmiwifi() {
	[ -z "$miwifi_info_enable" ] && return
	local last_getmiwifi_time=$(file_date "${dir}/miwifi_info")

	if [ "$last_getmiwifi_time" -ge "$device_info_helper_sleeptime" ]; then
		# 登录并获取 stok
		local miwifi_login_url="http://${miwifi_ip}/cgi-bin/luci/api/xqsystem/login"
		local login_response=$(run_with_tag curl -s -X POST "$miwifi_login_url" -d "username=admin&password=${miwifi_password}")
		local stok=$(echo "$login_response" | jq -r '.token')

		[ -z "$stok" ] && log_change "[WARN] $(translate "Login to MIWiFi failed, unable to get stok")" && return 1

		# 获取设备列表
		local device_list_url="http://${miwifi_ip}/cgi-bin/luci/;stok=$stok/api/misystem/devicelist"
		local device_list_response=$(run_with_tag curl -s "$device_list_url")
		#local parent=$(jq -r --arg ip "$ip" '.devices[] | select(.ip == $ip) | .mac' "$devices_json")

		# 保存信息
		echo "$device_list_response" | jq '
			.list | map({
				ip: .ip[0].ip,
				name: (if .name == .mac then "unknown" else .name end),
				mac: .mac,
				parent: .parent,
				type: (if .type == 1 then "2.4G" elif .type == 2 then "5G" else "LAN" end)
			})
		' > "${dir}/miwifi_info"

		miwifi_iplist=$(jq -r '.[] | select(.ip != "unknown" and .ip != "") | .ip' "${dir}/miwifi_info")
	else
		unset miwifi_iplist
	fi
}

# 从 openwrt 获取设备信息
getopenwrt() {
	[ -z "$openwrt_info_enable" ] && return
	local last_openwrt_time=$(file_date "${dir}/openwrt_info")
	if [ "$last_openwrt_time" -ge "$device_info_helper_sleeptime" ]; then
		# 初始化一个空数组（bash 数组）
		local json_array=()
		for op_host_ip in $op_host_ips; do
			local wlan_interfaces=$(run_with_tag ssh -o ConnectTimeout=5 -o StrictHostKeyChecking=no -i /root/.ssh/id_rsa root@$op_host_ip 'iw dev 2>/dev/null | grep Interface | awk '\''{print $2}'\''')
			local leases_info=$(run_with_tag ssh -o ConnectTimeout=5 -o StrictHostKeyChecking=no -i /root/.ssh/id_rsa root@$op_host_ip "cat /tmp/dhcp.leases 2>/dev/null")
			local arp_info=$(run_with_tag ssh -o ConnectTimeout=5 -o StrictHostKeyChecking=no -i /root/.ssh/id_rsa root@$op_host_ip "cat /proc/net/arp | grep '0x[26]' | grep -v '^169.254.' | grep -v '^$' | sort -u 2>/dev/null")
			local openwrt_arp_iplist+="$(echo "$arp_info" | awk '{print $1}')"$'\n'

			# 遍历每个接口
			for iface in $wlan_interfaces; do
				# 获取接口信息
				local info=$(run_with_tag ssh -o ConnectTimeout=5 -o StrictHostKeyChecking=no -i /root/.ssh/id_rsa root@$op_host_ip "iw dev $iface info 2>/dev/null")

				# 获取频道和频段
				local channel=$(echo "$info" | grep channel | awk '{print $2}')
				local band="unknown"
				if [[ -n "$channel" && "$channel" -ge 1 && "$channel" -le 14 ]]; then
					band="2.4G"
				elif [[ -n "$channel" && "$channel" -ge 36 ]]; then
					band="5G"
				fi

				# 获取连接的设备列表
				local devices=$(run_with_tag ssh -o ConnectTimeout=5 -o StrictHostKeyChecking=no -i /root/.ssh/id_rsa root@$op_host_ip "iw dev $iface station dump 2>/dev/null")

				# 逐行输出设备信息
				while read -r line; do
					if [[ "$line" =~ "Station" ]]; then
						# 提取设备的MAC地址
						local mac=$(echo "$line" | awk '{print $2}')

						# 查找设备名称
						local ip=$(echo "$arp_info" | awk -v mac="$mac" '$4 == mac {print $1; exit}')
						[ -z "$ip" ] && local ip=$(jq -r --arg mac "$mac" '.devices[] | select((.mac | ascii_downcase) == ($mac | ascii_downcase)) | .ip' "$devices_json")
						local name=$(echo "$leases_info" | awk -v mac="$mac" '$2 == mac {print $4; exit}')
						[ -z "$ip" ] && ip="unknown"
						[ -z "$name" ] && name="unknown"

						# 生成JSON格式的设备信息，并将其添加到数组中
						local json_device=$(jq -n \
							--arg ip "$ip" \
							--arg name "$name" \
							--arg mac "$mac" \
							--arg parent "$op_host_ip" \
							--arg type "$band" '
							{
								ip: $ip,
								name: $name,
								mac: $mac,
								parent: $parent,
								type: $type
							}')

						# 将生成的 JSON 设备信息添加到数组中
						json_array+=("$json_device")
					fi
				done <<< "$devices"
			done
		done
		# 使用 jq 输出最终的 JSON 数组
		echo "${json_array[@]}" | jq -s . > "${dir}/openwrt_info"
		openwrt_iplist=$(jq -r '.[] | select(.ip != "unknown" and .ip != "") | .ip' "${dir}/openwrt_info")
		openwrt_iplist+=$'\n'"$openwrt_arp_iplist"
	else
		unset openwrt_iplist
	fi
}

# 从 ROS 获取设备信息
getmikrotik() {
	[ -z "$mikrotik_info_enable" ] && return
	local last_getmikrotik_time=$(file_date "${dir}/mikrotik_info")

	if [ "$last_getmikrotik_time" -ge "$device_info_helper_sleeptime" ]; then
		lease_info=$(run_with_tag ssh -o ConnectTimeout=5 ${mikrotik_username}@${mikrotik_ip} "/ip dhcp-server lease print terse where status=bound")

		echo "$lease_info" | awk '
			BEGIN {
				print "["
				first_entry = 1
			}
			{
				# 初始化变量
				address = ""
				mac_address = ""
				host_name = ""

				# 遍历每行的键值对
				split($0, fields, " ")
				for (i in fields) {
					if (fields[i] ~ /^address=/) {
						split(fields[i], arr, "=")
						address = arr[2]
					}
					if (fields[i] ~ /^mac-address=/) {
						split(fields[i], arr, "=")
						mac_address = arr[2]
					}
					if (fields[i] ~ /^host-name=/) {
						split(fields[i], arr, "=")
						host_name = arr[2]
					}
				}

				# 如果提取到有效数据，则生成JSON
				if (address != "" && mac_address != "") {
					if (!first_entry) {
						print ","
					}
					printf "  {"
					printf "\"ip\": \"%s\", ", address
					printf "\"mac\": \"%s\", ", mac_address
					printf "\"name\": \"%s\"", host_name
					printf "}"
					first_entry = 0
				}
			}
			END {
				print "\n]"
			}
			' | jq > "${dir}/mikrotik_info"

		mikrotik_iplist=$(jq -r '.[] | select(.ip != "unknown" and .ip != "") | .ip' "${dir}/mikrotik_info")
	else
		unset mikrotik_iplist
	fi
}

# 在线设备列表
first() {
	# 注：此处为后台任务，无法操作父进程变量，故每个线程结束后都必须保存变量到文件
	# 耗时太长，定时推送不再检查
	local IPLIST=$(jq -r '.devices[] | select(.status == "online" or .status == "unknown") | .ip' "$devices_json" | sort -u)
	[ ! -f "${dir}/send_pid" ] && getgateway
	[ ! -f "${dir}/send_pid" ] && getmiwifi
	[ ! -f "${dir}/send_pid" ] && getopenwrt
	[ ! -f "${dir}/send_pid" ] && getmikrotik
	[ ! -f "${dir}/send_pid" ] && silent_run scanlocalip
	for ip in $IPLIST; do
		[ -n "$passive_mode" ] && [ "$passive_mode" -eq "1" ] && break
		# 获取一个令牌
		read -u 5
		{
			down "$ip"
			# 释放令牌
			echo >&5
		} &
	done
	wait

	local IP_INFO=$(ip addr show br-lan | grep 'inet ' | awk '{print $2}')
	[ -n "$IP_INFO" ] && local SUBNET=$(echo $IP_INFO | cut -d'/' -f1 | cut -d'.' -f1-3)
	[ -n "$SUBNET" ] && local IPLIST=$(cat /proc/net/arp | grep "0x2\|0x6" | awk '{print $1}' | grep -v "^169.254." | grep -v "^$" | sort -u | grep -oE "${SUBNET}\.[0-9]{1,3}") || local IPLIST=$(cat /proc/net/arp | grep "0x2\|0x6" | awk '{print $1}' | grep -v "^169.254." | grep -v "^$" | sort -u | grep -oE '[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}')

	local IPLIST+=$'\n'"$gateway_iplist"
	local IPLIST+=$'\n'"$miwifi_iplist"
	local IPLIST+=$'\n'"$openwrt_iplist"
	local IPLIST+=$'\n'"$mikrotik_info"
	local IPLIST+=$'\n'"$always_check_ip_list"
	local IPLIST=$(echo "$IPLIST" | grep -v "^$" | sort -u)
	for ip in $IPLIST; do
		[ -n "$passive_mode" ] && [ "$passive_mode" -eq "1" ] && break
		# 获取一个令牌
		read -u 5
		{
			up "$ip"
			# 释放令牌
			echo >&5
		} &
	done
	wait
}

# 检测设备上线
up() {
	local ip="$1"
	[ -z "$ip" ] && return 1

	jq -e --arg ip "$ip" '.devices[] | select(.ip == $ip and .status == "online") | .ip' "$devices_json" >/dev/null && return # 如果当前 IP 为在线，退出
	local mac=$(getmac "$ip")
	getping "$ip" "$mac" "$up_timeout" "1"
	local ping_online=$?

	# 连通
	if [ "$ping_online" -eq "0" ]; then
		silent_run LockFile lock
		local time_up=$(date +%s)

		# 如果 IP 不存在，或者 http_access 键值为空，检查 http_access 参数
		# 检查 http_access 参数时间过长，考虑只在第一次连通时检查
		#jq -e --arg ip "$ip" 'any(.devices[]; .ip == $ip and (.http_access | length == 0)) or (.devices | map(select(.ip == $ip)) | length == 0)' "$devices_json" >/dev/null && local http_access=$(check_http_access "$ip")

		# 如果是待二次离线检测 unknown 设备，json 文件中依然有信息，修改在线状态为 online
		if [ "$mac" != "unknown" ] && jq -e --arg ip "$ip" --arg mac "$mac" '.devices[] | select(.ip == $ip and (.mac | ascii_downcase) == ($mac | ascii_downcase)) | .mac' "$devices_json" >/dev/null; then
			jq --arg ip "$ip" --arg mac "$mac" '.devices |= map(if .ip == $ip then .mac = $mac | .status = "online" else . end)' "$devices_json" >"$tmp_devices_json" && mv "$tmp_devices_json" "$devices_json"
		# up
		else
			# 剔除（重置）流量数据
			usage down ${ip}
			# 删除当前 IP 对应的键值，重新写入，否则要启用 unique_by(.ip) 去重
			jq --arg ip "$ip" 'del(.devices[] | select(.ip == $ip))' "$devices_json" >"$tmp_devices_json" && mv "$tmp_devices_json" "$devices_json"
			local name=$(getname "$ip" "$mac")
			local interface=$(getinterface "ip" "$mac")
			local http_access=$(check_http_access "$ip")
			new_device='{
				"name": "'"${name}"'",
				"ip": "'"${ip}"'",
				"mac": "'"${mac}"'",
				"interface": "'"${interface}"'",
				"uptime": "'"${time_up}"'",
				"usage": "",
				"http_access": "'"${http_access}"'",
				"status": "online"
			}'
			jq --argjson newdevice "$new_device" '.devices += [$newdevice]' "$devices_json" >"$tmp_devices_json" && mv "$tmp_devices_json" "$devices_json"

			blackwhitelist ${mac}
			local ip_blackwhite=$?

			[ -f "${dir}/send_enable.lock" ] || [ -z "$notification_online" ] || [ -z "$ip_blackwhite" ] && silent_run LockFile unlock && return
			[ -z "$ip_blackwhite" ] || [ "$ip_blackwhite" -ne "0" ] && silent_run LockFile unlock && return

			[ -f "${dir}/title" ] && local title=$(cat "${dir}/title")
			[ -f "${dir}/content" ] && local content=$(cat "${dir}/content")

			if [ -z "$title" ]; then
				local title=$(translate "%s connected to your router" "$name")
				local content_title="${str_title_start}$(translate "New device connected")${str_title_end}"
			elif echo "$title" | grep -q "$(translate "connected to your router")"; then
				local title="${name} ${title}"
			else
				local title="$(translate "Device status changed")"
				local content_title="${str_title_start}$(translate "New device connected")${str_title_end}"
			fi

			content="\
${str_splitline}\
${content_title}\
${str_linefeed}${str_tab} - $(translate "Client Name:")${str_space}${str_space}${str_space}${str_space}${str_space}${name}\
${str_linefeed}${str_tab} - $(translate "Client IP:")${str_space}${str_space}${str_space}${str_space}${ip}\
${str_linefeed}${str_tab} - $(translate "Client MAC:")${str_space}${str_space}${str_space}${str_space}${mac}\
${str_linefeed}${str_tab} - $(translate "Network Interface:")${str_space}${str_space}${str_space}${str_space}${str_space}${interface}"

			log_change "${disturb_text} $(translate "New device %s %s connected" "$name" "$ip")"

			[ -n "$title" ] && echo "$title" >"${dir}/title"
			[ -n "$content" ] && echo -n "$content" >>"${dir}/content"
		fi
		silent_run LockFile unlock
	fi
}

# 检测设备离线
down() {
	local ip="$1"
	[ -z "$ip" ] && return 1
	local mac=$(getmac "$ip")

	tmp_timeout=$down_timeout && tmp_retry_count=$timeout_retry_count
	[ -n "$only_timeout_push" ] && blackwhitelist "$mac"
	local ip_blackwhite=$? && [ "$ip_blackwhite" -ne "0" ] && tmp_timeout=10 && tmp_retry_count=2
	getping "$ip" "$mac" "$tmp_timeout" "$tmp_retry_count"
	local ping_online=$?
	silent_run LockFile lock

	# 离线，置入待验证区
	if [ "$ping_online" -eq "1" ]; then
		# 修改为 unknown
		if jq -e --arg ip "$ip" '.devices[] | select(.ip == $ip and .status == "online") | .ip' "$devices_json" >/dev/null; then
			jq --arg ip "$ip" '.devices |= map(if .ip == $ip then .status = "unknown" else . end)' "$devices_json" >"$tmp_devices_json" && mv "$tmp_devices_json" "$devices_json"
		#这里其实无需再判断，只有 status == "online" 和 status == "unknown" 的设备会使用 down $ip
		#elif jq -e --arg ip "$ip" '.devices[] | select(.ip == $ip and .status == "unknown") | .ip' "$devices_json" >/dev/null; then
		else
			jq --arg ip "$ip" '.devices |= map(if .ip == $ip then .status = "offline" else . end)' "$devices_json" >"$tmp_devices_json" && mv "$tmp_devices_json" "$devices_json"
		fi
	# 更新设备信息
	else
		local name=$(getname "$ip" "$mac")
		local interface=$(getinterface "ip" "$mac")

		if ! jq -e --arg ip "$ip" --arg mac "$mac" --arg name "$name" --arg interface "$interface" '.devices[] | select(.ip == $ip and (.mac | ascii_downcase) == ($mac | ascii_downcase) and .name == $name and .interface == $interface) | .ip' "$devices_json" >/dev/null; then
			jq --arg ip "$ip" --arg new_mac "$mac" --arg new_name "$name" --arg new_interface "$interface" '
				.devices |= map(
					if .ip == $ip then
						.mac = $new_mac |
						.name = $new_name |
						.interface = $new_interface
					else .
					end
				)
			' "$devices_json" >"$tmp_devices_json" && mv "$tmp_devices_json" "$devices_json"
		fi

		# 读取 type 和 parent
		[ -f "${dir}/openwrt_info" ] && {
			local tmp_type=$(jq -r --arg mac "$mac" '.[] | select((.mac | ascii_downcase) == ($mac | ascii_downcase)) | .type' "${dir}/openwrt_info")
			local tmp_parent=$(jq -r --arg mac "$mac" '.[] | select((.mac | ascii_downcase) == ($mac | ascii_downcase)) | .parent' "${dir}/openwrt_info")
		}

		[ -f "${dir}/miwifi_info" ] && [ -z "$tmp_type" ] && {
			local tmp_type=$(jq -r --arg ip "$ip" '.[] | select(.ip == $ip) | .type' "${dir}/miwifi_info")
			local tmp_parent=$(jq -r --arg ip "$ip" '.[] | select(.ip == $ip) | .parent' "${dir}/miwifi_info")
		}

		[ -f "${dir}/gateway_info" ] && [ -z "$tmp_type" ] && {
			local tmp_type=$(jq -r --arg ip "$ip" '.[] | select(.ip == $ip) | .type' "${dir}/gateway_info")
			local tmp_parent=$(jq -r --arg ip "$ip" '.[] | select(.ip == $ip) | .parent' "${dir}/gateway_info")
		}

		[ -n "$tmp_type" ] && jq --arg ip "$ip" --arg type "$tmp_type" --arg parent "$tmp_parent" '
			.devices |= map(
				if .ip == $ip then
					.type = $type |
					.parent = $parent
				else
					.
				end
			)
		' "$devices_json" >"$tmp_devices_json" && mv "$tmp_devices_json" "$devices_json"
	fi
	silent_run LockFile unlock
}

# 当前设备列表
current_device() {
	# 定时推送中已有设备列表选项，不应被精简模式设置项控制
	[ ! -f "${dir}/send_pid" ] && {
		(echo "$lite_enable" | grep -q "content") || (echo "$lite_enable" | grep -q "device") && return
	}
	local logrow=$(jq '.devices | map(select(.status == "online")) | length' "$devices_json")
	local has_usage=0
	[ -f "${dir}/usage.db" ] && has_usage=1

	[ "$logrow" -eq "0" ] && {
		local explain="${send_content}\
${str_splitline}\
${str_title_start}$(translate "Currently no online devices")${str_title_end}"
		content="${content}${explain}${header}"
		return
	}

	local explain="${str_splitline}\
${str_title_start}$(translate "Currently %d online devices, details below" "$logrow")${str_title_end}"

	local IPLIST
	if [ -z "$defaultSortColumn" ] || [ "$defaultSortColumn" = "ip" ]; then
		# 按 IP 排序
		IPLIST=$(jq -r '.devices[] | select(.status == "online") | .ip' "$devices_json" | sort -uV)
	else
		# 按在线时间（短到长）
		IPLIST=$(jq -r '.devices | sort_by(-(.uptime | tonumber)) | .[] | select(.status == "online") | .ip' "$devices_json")
	fi

	# 定义数组，预处理
	declare -a ips totals time_onlines names
	local idx=0

	for ip in $IPLIST; do
		local mac total name time_up time_online
		mac=$(getmac "$ip")
		if [ "$has_usage" -eq 1 ]; then
			total=$(usage get "$mac")
		else
			total=""
		fi
		name=$(getname "$ip" "$mac")
		name=$(cut_str "$name" "12")
		time_up=$(jq -r --arg ip "$ip" '.devices[] | select(.ip == $ip) | .uptime' "$devices_json")
		time_online=$(time_for_humans "$(( $(date +%s) - time_up ))")

		ips[idx]="$ip"
		totals[idx]="$total"
		time_onlines[idx]="$time_online"
		names[idx]="$name"
		idx=$((idx + 1))
	done

	# 计算每列的最大宽度
	local ip_max=0 total_max=0 time_online_max=0
	for i in "${!ips[@]}"; do
		local len_ip len_total len_time_online
		len_ip=$(length_str "${ips[i]}")
		[ "$len_ip" -gt "$ip_max" ] && ip_max=$len_ip

		if [ "$has_usage" -eq 1 ]; then
			len_total=$(length_str "${totals[i]}")
			[ "$len_total" -gt "$total_max" ] && total_max=$len_total
		fi

		len_time_online=$(length_str "${time_onlines[i]}")
		[ "$len_time_online" -gt "$time_online_max" ] && time_online_max=$len_time_online
	done

	# 每列宽度 = 最大宽度 + 2*${str_space}
	ip_max=$((ip_max + 2))
	if [ "$has_usage" -eq 1 ]; then
		total_max=$((total_max + 2))
	fi
	time_online_max=$((time_online_max + 2))

	# 如果字符长度小于最大宽度，用 str_space 填充
	pad_field() {
		local field="$1"
		local target_width=$2
		local current_width=$(length_str "$field")
		local pad_count=$(( target_width - current_width ))
		local pad=$(printf "%${pad_count}s" "" | sed "s/ /${str_space}/g")
		printf "%s%s" "$field" "$pad"
	}

	# 表头
	local explain="${str_splitline}\
${str_title_start}$(translate "Currently %d online devices, details below" "$logrow")${str_title_end}"

	local header="${str_linefeed}${str_tab}"
	header+=$(pad_field "$(translate "IP Address")" "$ip_max")"${str_space}"
	if [ "$has_usage" -eq 1 ]; then
		header+=$(pad_field "$(translate "Total Traffic")" "$total_max")
	fi
	header+=$(pad_field "$(translate "Uptime")" "$time_online_max")
	header+=$(translate "Client Name")

	content="${content}${explain}${header}"

	# 内容
	for i in "${!ips[@]}"; do
		local line="${str_linefeed}${str_tab}"
		line+=$(pad_field "${ips[i]}" "$ip_max")
		if [ "$has_usage" -eq 1 ]; then
			line+=$(pad_field "${totals[i]}" "$total_max")
		fi
		line+=$(pad_field "${time_onlines[i]}" "$time_online_max")
		line+="${names[i]}"

		content="${content}${line}"
	done

	[ -f "${dir}/send_pid" ] && echo "$content" >"$output_dir/current_device" && unset content
}

# 无人值守任务
unattended() {
	[ -z "$unattended_enable" ] || [ "$unattended_enable" -ne "1" ] && return
	[ -n "$unattended_only_on_disturb_time" ] && [ "$unattended_only_on_disturb_time" -eq "1" ] && [ -z "$sheep_starttime" ] && return
	geterrdevicealiases
	[ $? -eq "1" ] && return

	if [ -n "$unattended_autoreboot_mode" ]; then
		local interfaceuptime=$(getinterfaceuptime)
		if [ -n "$autoreboot_system_uptime" ] && [ $(cat /proc/uptime | awk -F. '{run_hour=$1/3600;printf("%d",run_hour)}') -ge "$autoreboot_system_uptime" ] && [ "$unattended_autoreboot_mode" -eq "1" ]; then
			log_change "[INFO] $(translate "Rebooting router...")"
			cat ${logfile} >/usr/share/wechatpush/errlog
			sleep 2 && reboot && exit
		elif [ -n "$autoreboot_network_uptime" ] && [ -n "$interfaceuptime" ] && [ $(echo "$interfaceuptime" | awk -F. '{run_hour=$1/3600;printf("%d",run_hour)}') -ge "$autoreboot_network_uptime" ] && [ "$unattended_autoreboot_mode" -eq "2" ]; then
			log_change "[INFO] $(translate "Re-establishing PPPoE connection...")"
			ifup wan >/dev/null 2>&1
			sleep 60
		fi
	fi
}

# 设备异常流量检测
get_client_usage() {
	[ -z "$client_usage" ] && return
	[ "$client_usage" -ne "1" ] && return
	[ -z "$client_usage_max" ] && return

	[ -z "$get_client_usage_time" ] && get_client_usage_time=$(date +%s)
	(echo $client_usage_max | sed -r 's/.*(.)$/\1/' | grep -q "K\|k") && client_usage_max=$(expr ${client_usage_max%?} \* 1024)
	(echo $client_usage_max | sed -r 's/.*(.)$/\1/' | grep -q "M\|m") && client_usage_max=$(expr ${client_usage_max%?} \* 1048576)
	(echo $client_usage_max | sed -r 's/.*(.)$/\1/' | grep -q "G\|g") && client_usage_max=$(expr ${client_usage_max%?} \* 1073741824)
	[ -z "$client_usage_disturb" ] && client_usage_disturb="0"
	[ "$client_usage_disturb" -eq "0" ] && local MACLIST=$(jq -r '.devices[] | select(.status == "online") | .mac' "$devices_json" | sort -u)
	[ "$client_usage_disturb" -eq "1" ] && [ -n "$client_usage_whitelist" ] && local MACLIST=$(echo "$client_usage_whitelist")
	[ -z "$MACLIST" ] && return

	if [ "$(($(date +%s) - $get_client_usage_time))" -ge "60" ]; then
		>${dir}/client_usage_aliases
		for mac in $MACLIST; do
			echo "$mac" $(usage get ${mac} bytes) >>${dir}/client_usage_aliases
			[ -f "${dir}/old_client_usage_aliases" ] && get_client_usage_bytes=$(cat ${dir}/old_client_usage_aliases | grep -i -w $mac | awk '{print $2}' | grep -v "^$" | sort -u | head -n1) || continue

			[ -z "$get_client_usage_bytes" ] && get_client_usage_bytes="0"
			if [ "$(($(usage get ${mac} bytes) - $get_client_usage_bytes))" -ge "$client_usage_max" ]; then
				local ip=$(jq -r --arg mac "$mac" '.devices[] | select((.mac | ascii_downcase) == ($mac | ascii_downcase) and .status == "online") | .ip' "$devices_json")
				local ip_name=$(getname ${ip} ${mac})
				local tmp_usage=$(bytes_for_humans $(expr $(usage get ${mac} bytes) - ${get_client_usage_bytes}))
				local time_up=$(jq -r --arg mac "$mac" '.devices[] | select((.mac | ascii_downcase) == ($mac | ascii_downcase) and .status == "online") | .uptime' "$devices_json")
				local ip_total=$(usage get $mac) && [ -n "$ip_total" ] && local ip_total="${str_linefeed}${str_tab} - $(translate "Total Traffic:") ${str_space}${str_space}${str_space}${str_space}${ip_total}"
				local time1=$(date +%s)
				local time1=$(time_for_humans $(expr ${time1} - ${time_up}))

				if [ -z "$title" ]; then
					title=$(translate "%s abnormal traffic" "$name")
					local content_title="${str_splitline}${str_title_start}$(translate "Device abnormal traffic")${str_title_end}"
				elif echo "$title" | grep -q "$(translate "abnormal traffic")"; then
					title="${ip_name} ${title}"
				else
					title="$(translate "Device status changed")"
					local content_title="${str_splitline}${str_title_start}$(translate "Device abnormal traffic")${str_title_end}"
				fi

				content="\
${content}\
${content_title}\
${str_linefeed}${str_tab} - $(translate "Client Name:")${str_space}${str_space}${str_space}${str_space}${str_space}${ip_name}\
${str_linefeed}${str_tab} - $(translate "Client IP:")${str_space}${str_space}${str_space}${str_space}${ip}\
${str_linefeed}${str_tab} - $(translate "Client MAC:")${str_space}${str_space}${str_space}${str_space}${mac}${ip_total}\
${str_linefeed}${str_tab} - $(translate "Traffic in 1 minute:")${str_space}${str_space}${str_space}${tmp_usage}\
${str_linefeed}${str_tab} - $(translate "Uptime:")${str_space}${str_space}${str_space}${str_space}${time1}"
			fi
		done
		cat ${dir}/client_usage_aliases >${dir}/old_client_usage_aliases
		get_client_usage_time=$(date +%s)
	fi
}

# ------------------------------------
# 自动封禁相关
#

# 添加白名单，懒得写删除项和信息显示了，纯粹就是懒
add_ip_white() {
	[ -n "$port_knocking_enable" ] && [ "$port_knocking_enable" -eq "1" ] || return
	[ -z "$2" ] && timeout=$login_ip_white_timeout || timeout=$2
	# 检查 IP 版本
	unset ipset_name
	(echo "$1" | grep -Eq '^([0-9]{1,3}\.){3}[0-9]{1,3}$') && local ipset_name="wechatpush_whitelist"
	(echo "$1" | grep -Eq '^([\da-fA-F0-9]{1,4}(:{1,2})){1,15}[\da-fA-F0-9]{1,4}$') && local ipset_name="wechatpush_whitelistv6"
	[ -z "$ipset_name" ] && log_change "[ERROR] $(translate "Whitelist add failed, IP format error")" && return

	[ -n "$nftables_version" ] && {
		nft delete element inet fw4 $ipset_name { $1 } >/dev/null 2>&1
		nft add element inet fw4 $ipset_name { $1 expires ${timeout}s } #没找到刷新时间的命令，删除再添加
	} || {
		ipset -exist add $ipset_name $1 timeout $timeout
	}
}

# 初始化白名单
init_ip_white() {
	[ -n "$port_knocking_enable" ] && [ "$port_knocking_enable" -eq "1" ] || return
	# 设置 IP 版本变量
	if [ $1 == "ipv4" ]; then
		ipset_name="wechatpush_whitelist"
		ip_version="ip"
	elif [ $1 == "ipv6" ]; then
		ipset_name="wechatpush_whitelistv6"
		ip_version="ip6"
		nat_table_cmd="family inet6"
	fi

	if [ -n "$nftables_version" ]; then
		! nft list set inet fw4 $ipset_name >/dev/null 2>&1 && nft add set inet fw4 $ipset_name { type ${1}_addr\; flags timeout\; timeout ${login_ip_white_timeout}s\; }
		nft -- add chain inet fw4 wechatpush_dstnat { type nat hook prerouting priority -100 \; }
		nft add chain inet fw4 wechatpush_srcnat { type nat hook postrouting priority 100 \; }
	else
		! ipset list $ipset_name >/dev/null 2>&1 && ipset create $ipset_name hash:ip timeout $login_ip_white_timeout $nat_table_cmd >/dev/null 2>&1
	fi

	# 端口放行
	if [ -n "$login_port_white" ]; then
		local login_port_white=$(echo "$login_port_white" | sed 's/ //g' | sed 's/,/, /g') 2>/dev/null
		if [ -n "$nftables_version" ]; then
			local count_accept_rules=$(nft list ruleset | grep -c "tcp dport.* ${login_port_white}.* $ip_version saddr @${ipset_name} counter packets .* accept comment \"\!wechatpush Accept rule\"")
			if [ $count_accept_rules -eq 0 ]; then
				nft insert rule inet fw4 input tcp dport { $login_port_white } $ip_version saddr @$ipset_name counter accept comment \"\!wechatpush Accept rule\" >/dev/null 2>&1
			elif [ $count_accept_rules -ne 1 ]; then
				local i=0
				local handles=$(nft --handle list ruleset | grep "\!wechatpush Accept rule" | grep -v "tcp dport.* ${login_port_white}.* $ip_version saddr @${ipset_name} counter packets .* accept comment \"\!wechatpush Accept rule\"" | awk '{print $NF}')
				for handle in $handles; do
					[ $i -eq 0 ] && i=1 && continue
					nft delete rule $handle
				done
			fi
		else
			${ip_version}tables -C INPUT -m set --match-set $ipset_name src -p tcp -m multiport --dport $login_port_white -j ACCEPT >/dev/null 2>&1 || ${ip_version}tables -I INPUT -m set --match-set $ipset_name src -p tcp -m multiport --dport $login_port_white -j ACCEPT >/dev/null 2>&1
		fi
	fi
	unset handle
	# 端口转发
	while IFS= read -r port_forward; do
		port_forward=$(echo "$port_forward" | sed 's/,/ /g') 2>/dev/null
		[ $(echo $port_forward | awk -F" " '{print NF}') -ne "4" ] && continue
		local src_ip=$(echo ${port_forward} | awk '{print $1}')
		local src_port=$(echo ${port_forward} | awk '{print $2}')
		local dst_ip=$(echo ${port_forward} | awk '{print $3}')
		local dst_port=$(echo ${port_forward} | awk '{print $4}')
		if [ -n "$nftables_version" ]; then
			! nft list ruleset | grep "$ip_version saddr @${ipset_name} tcp dport $src_port counter .* dnat $ip_version to $dst_ip:$dst_port comment \"\!wechatpush DNAT rule\"" >/dev/null 2>&1 && nft insert rule inet fw4 wechatpush_dstnat meta nfproto $1 $ip_version saddr @${ipset_name} tcp dport $src_port counter dnat to "$dst_ip:$dst_port" comment \"\!wechatpush DNAT rule\" >/dev/null 2>&1
			! nft list ruleset | grep "$ip_version daddr $dst_ip tcp dport $dst_port counter .* snat $ip_version to $src_ip comment \"\!wechatpush SNAT rule\"" >/dev/null 2>&1 && nft insert rule inet fw4 wechatpush_srcnat $ip_version daddr $dst_ip tcp dport $dst_port counter snat to $src_ip comment \"\!wechatpush SNAT rule\" >/dev/null 2>&1
		else
			${ip_version}tables -t nat -C PREROUTING -m set --match-set $ipset_name src -p tcp --dport $src_port -j DNAT --to-destination "$dst_ip:$dst_port" >/dev/null 2>&1 || ${ip_version}tables -t nat -I PREROUTING -m set --match-set $ipset_name src -p tcp --dport $src_port -j DNAT --to-destination "$dst_ip:$dst_port" >/dev/null 2>&1
			${ip_version}tables -t nat -C POSTROUTING -m set --match-set $ipset_name src -p tcp -d $dst_ip --dport $dst_port -j SNAT --to-source $src_ip >/dev/null 2>&1 || ${ip_version}tables -t nat -I POSTROUTING -m set --match-set $ipset_name src -p tcp -d $dst_ip --dport $dst_port -j SNAT --to-source $src_ip >/dev/null 2>&1
		fi
	done <<<"$login_port_forward_list"
	unset port_forward
}

# 初始化黑名单规则
init_ip_black() {
	[ -n "$login_web_black" ] && [ "$login_web_black" -eq "1" ] || return
	# 设置 IP 版本变量
	if [ $1 == "ipv4" ]; then
		ipset_name="wechatpush_blacklist"
		ip_version="ip"
	elif [ $1 == "ipv6" ]; then
		ipset_name="wechatpush_blacklistv6"
		ip_version="ip6"
		nat_table_cmd="family inet6"
	fi

	[ -n "$nftables_version" ] && {
		! nft list set inet fw4 ${ipset_name} >/dev/null 2>&1 && nft add set inet fw4 ${ipset_name} { type ${1}_addr\; flags timeout\; timeout ${login_ip_black_timeout}s\; }
		! nft list ruleset | grep "$ip_version saddr @${ipset_name} counter .* comment \"\!wechatpush Drop rule\"" >/dev/null 2>&1 && nft insert rule inet fw4 input $ip_version saddr @${ipset_name} counter drop comment \"\!wechatpush Drop rule\" >/dev/null 2>&1
	} || {
		ipset list $ipset_name >/dev/null 2>&1 || ipset create ${ipset_name} hash:ip timeout ${login_ip_black_timeout} ${nat_table_cmd} >/dev/null 2>&1
		${ip_version}tables -C INPUT -m set --match-set ${ipset_name} src -j DROP >/dev/null 2>&1 || ${ip_version}tables -I INPUT -m set --match-set ${ipset_name} src -j DROP >/dev/null 2>&1
	}
}

# 添加黑名单
add_ip_black() {
	local login_ip=$1
	[ -z "$login_ip" ] && return 0
	echo "$login_ip_white_list" | grep -w -q "$login_ip" && return 1
	# 检查 IP 版本
	unset ipset_name
	(echo "$login_ip" | grep -Eq '^([0-9]{1,3}\.){3}[0-9]{1,3}$') && ipset_name="wechatpush_blacklist"
	(echo "$login_ip" | grep -Eq '^([\da-fA-F0-9]{1,4}(:{1,2})){1,15}[\da-fA-F0-9]{1,4}$') && ipset_name="wechatpush_blacklistv6"
	[ -z "$ipset_name" ] && sed -i "/^$login_ip /d" "$ip_blacklist_path" && log_change "[WARN] $(translate "Failed to add to blacklist, invalid IP format: %s (removed from list)" "$login_ip")" && return 1

	! cat "$ip_blacklist_path" | grep -q -w -i $login_ip && echo "$login_ip timeout $login_ip_black_timeout" >>"$ip_blacklist_path"

	[ -n "$nftables_version" ] && {
		nft list set inet fw4 ${ipset_name} | grep -qw "${login_ip}" && return 1 # IP 已存在
		nft add element inet fw4 $ipset_name { $login_ip expires ${login_ip_black_timeout}s } >/dev/null 2>&1
	} || {
		ipset -exist add $ipset_name $login_ip timeout ${login_ip_black_timeout} >/dev/null 2>&1
	}
}

# 移出黑名单
del_ip_black() {
	[ -z "$1" ] && return
	sed -i "/^${1}/d" ${ip_blacklist_path}

	# 检查 IP 版本
	unset ipset_name
	(echo "$1" | grep -Eq '^([0-9]{1,3}\.){3}[0-9]{1,3}$') && ipset_name="wechatpush_blacklist"
	(echo "$1" | grep -Eq '^([\da-fA-F0-9]{1,4}(:{1,2})){1,15}[\da-fA-F0-9]{1,4}$') && ipset_name="wechatpush_blacklistv6"
	[ -z "$ipset_name" ] && return

	[ -n "$nftables_version" ] && {
		nft delete element inet fw4 ${ipset_name} { $1 } >/dev/null 2>&1
	} || {
		ipset list ${ipset_name} >/dev/null 2>&1 && ipset -! del ${ipset_name} ${1}
	}
}

# 设置防火墙列表
set_ip_black() {
	# 检查换行，避免出错
	[ $(tail -n1 "${ip_blacklist_path}" | wc -l) -eq "0" ] && echo -e >>${ip_blacklist_path}

	# 从 ip_blacklist 文件逐行添加黑名单，add_ip_black() 处验证是否重复，此处不在验证
	for ip_black in $(cat ${ip_blacklist_path} | awk '{print $1}'); do
		add_ip_black "$ip_black"
	done
	# 当 ip_blacklist 文件清除 IP 时，从集合中清除 IP
	[ -n "$nftables_version" ] && fw_info_blacklist=$(nft list set inet fw4 wechatpush_blacklist | tr -d '\n' | grep -oE 'elements = \{[^}]*\}' | grep -oE '[^{}]+ expires [^,}]+[,\}]' | tr ',}' '\n' | tr -s ' ' | sed -e 's/^[[:space:]]*//')
	[ -n "$nftables_version" ] && fw_info_blacklistv6=$(nft list set inet fw4 wechatpush_blacklistv6 | tr -d '\n' | grep -oE 'elements = \{[^}]*\}' | grep -oE '[^{}]+ expires [^,}]+[,\}]' | tr ',}' '\n' | tr -s ' ' | sed -e 's/^[[:space:]]*//')
	[ -z "$nftables_version" ] && fw_info_blacklist=$(ipset list wechatpush_blacklist | grep "timeout" 2>/dev/null)
	[ -z "$nftables_version" ] && fw_info_blacklistv6=$(ipset list wechatpush_blacklistv6 | grep "timeout" 2>/dev/null)

	[ -n "$fw_info_blacklist" ] && [ -n "$fw_info_blacklistv6" ] && combined_fw_info_blacklist="${fw_info_blacklist}\n${fw_info_blacklistv6}"
	[ -z "$fw_info_blacklist" ] && combined_fw_info_blacklist="${fw_info_blacklistv6}" || combined_fw_info_blacklist="${fw_info_blacklist}"

	while IFS= read -r ip_black_info; do
		ip_black=$(echo "$ip_black_info" | grep -Eo "[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}")
		[ -z "$ip_black" ] && ip_black=$(echo "$ip_black_info" | grep -Eo "([\da-fA-F0-9]{1,4}(:{1,2})){1,15}[\da-fA-F0-9]{1,4}")
		[ -z "$ip_black" ] && continue
		cat ${ip_blacklist_path} | grep -q -w -i ${ip_black} && sed -i "/^${ip_black}/d" ${ip_blacklist_path} && echo ${ip_black_info} >>${ip_blacklist_path} || del_ip_black ${ip_black}
	done <<<"$combined_fw_info_blacklist"
}

# 监听登录事件（封装为函数，拒绝调试模式下日志刷屏）
monitor_logins() {
	if [ -n "$web_logged" ] || [ -n "$ssh_logged" ] || [ -n "$web_login_failed" ] || [ -n "$ssh_login_failed" ]; then
		# 声明关联数组
		declare -A web_login_counts
		declare -A ssh_login_counts
		declare -A web_failed_counts
		declare -A ssh_failed_counts

		# 子进程的信号处理函数
		cleanup_child() {
			kill $child_pid >/dev/null 2>&1
			wait $child_pid >/dev/null 2>&1
			rm -f "${dir}/child_pid" >/dev/null 2>&1
			exit 0
		}

		# 设置信号处理
		trap cleanup_child SIGINT SIGTERM

		(
			# 监听系统日志，-f 表示跟随实时日志，-p 表示日志级别为 notice
			run_with_tag logread -f -p notice | while IFS= read -r line; do
				[ -n "$web_logged" ] && {
					web_login_ip=$(echo "$line" | grep -i "accepted login" | awk '{print $NF}')
					[ -n "$web_login_ip" ] && process_login "$web_login_ip" $(echo "$line" | awk '{print $4}') web_login_counts
				}

				[ -n "$ssh_logged" ] && {
					ssh_login_ip=$(echo "$line" | grep -i "Password auth succeeded\|Pubkey auth succeeded" | awk '{print $NF}' | sed -nr 's#^(.*):.[0-9]{1,5}#\1#gp' | sed -e 's/%.*//')
					[ -n "$ssh_login_ip" ] && process_login "$ssh_login_ip" $(echo "$line" | awk '{print $4}') ssh_login_counts
				}

				[ -n "$web_login_failed" ] && {
					web_failed_ip=$(echo "$line" | grep -i "failed login" | awk '{print $NF}')
					[ -n "$web_failed_ip" ] && process_login "$web_failed_ip" $(echo "$line" | awk '{print $4}') web_failed_counts
				}

				[ -n "$ssh_login_failed" ] && {
					# 匹配特定的 SSH 登录失败情况并提取 IP 地址和时间
					ssh_failed_ip=$(echo "$line" | grep -iE "Bad password attempt|Login attempt for nonexistent user|Max auth tries reached" | awk '{print $NF}' | sed -nr 's#^(.*):[0-9]{1,5}#\1#gp' | sed -e 's/%.*//')

					# 如果未能提取到 IP，从日志标识符提取失败用户的 ID，并再次提取 IP
					if [ -z "$ssh_failed_ip" ]; then
						ssh_failed_num=$(echo "$line" | sed -n 's/.*authpriv\.warn dropbear\[\([0-9]\+\)\]: Login attempt for nonexistent user/\1/p')
						[ -n "$ssh_failed_num" ] && ssh_failed_ip=$(logread notice | grep "authpriv\.info dropbear\[${ssh_failed_num}\].*Child connection from" | awk '{print $NF}' | sed -nr 's#^(.*):[0-9]{1,5}#\1#gp' | sed -e 's/%.*//' | tail -n 1)
					fi

					# 如果成功提取到 IP 地址，调用 process_login 处理
					[ -n "$ssh_failed_ip" ] && process_login "$ssh_failed_ip" $(echo "$line" | awk '{print $4}') ssh_failed_counts
				}

			done
		) &
		# 分离子shell，避免影响 wait
		child_pid=$!
		sleep 1
		disown "$child_pid"
		echo $child_pid >"${dir}/child_pid"
	fi
}

# 处理登录事件
# 参数:
#   $1: IP
#   $2: 日志时间 - 从日志中读取而不是使用当前时间，避免秒对应不上
#   $3: 数组名 - 记录 IP 和登录次数的关联数组名
process_login() {
	local login_ip=$1
	local login_time=$2
	local -n login_counts=$3

	# 如果数组中不存在此 IP，初始化为 0
	if [ -z "${login_counts["$login_ip"]}" ]; then
		login_counts["$login_ip"]=0
	fi
	# +1
	login_counts["$login_ip"]=$((login_counts["$login_ip"] + 1))
	local count=${login_counts["$login_ip"]}

	# 封禁
	if [[ $count -ge $login_max_num && ("$3" == "web_failed_counts" || "$3" == "ssh_failed_counts") ]]; then
		add_ip_black ${login_ip} && {
			unset login_counts["$login_ip"]
			login_send "$login_ip" "$login_time" "$3"
		}
	fi

	# 正常登录
	if [[ "$3" == "web_login_counts" || "$3" == "ssh_login_counts" ]]; then
		add_ip_white ${login_ip}
		del_ip_black ${login_ip} # 白名单已经优先于黑名单，但白名单集合有超时限制，防止下次修改代码忘记，上保险
		unset web_failed_counts["$login_ip"]
		unset ssh_failed_counts["$login_ip"]
		unset login_counts["$login_ip"]
		login_send "$login_ip" "$login_time" "$3"
	fi
	[ "${#login_counts[@]}" -gt "100" ] && login_counts=("${login_counts[@]: -100}")
}

# ------------------------------------
# 信息推送相关
#
# 发送定时数据
send() {
	pid=$$
	echo "$pid" >"${dir}/send_pid"
	log_change "[INFO] $(translate "Create scheduled task")"
	>"${dir}/send_enable.lock"
	disturb
	disturb_RETVAL=$?
	[ "$disturb_RETVAL" -ne "0" ] && log_change "${disturb_text} $(translate "Do not disturb time, exit scheduled push")" && return
	get_config "send_title" "send_notification"

	[ -z "$1" ] && cpu_load=$(getcpu)
	[ -z "$1" ] && service_status=$(ubus call service list '{"name": "wechatpush"}' | grep -o '"running": true')
	# 只有当主程序没有运行，且定时任务中选择了推送设备列表，才进行在线设备扫描
	[ -z "$1" ] && [ -z "$service_status" ] && echo "$send_notification" | grep -q "client_list" && first &
	if [ -z "$1" ] && (echo "$send_notification" | grep -q "router_status"); then
		cat /proc/loadavg | awk '{print $1" "$2" "$3}' >"$output_dir/systemload" &
		free -m | sed -n '2p' | awk '{printf "%.2f%%\n",($3/$2)*100}' >"$output_dir/mem_usage" &
		curl -o /dev/null --connect-timeout 5 -s -w %{http_code} www.google.com >"$output_dir/Qwai" &

		system_uptime=$(cat /proc/uptime | awk '{print int($1)}')
		[ -n "$system_uptime" ] && system_status_time=$(translate "Uptime: %dd %dh %dm %ds" "$((system_uptime / 86400))" "$(((system_uptime % 86400) / 3600))" "$(((system_uptime % 3600) / 60))" "$((system_uptime % 60))")

		systeminfo_enable="1"
	fi
	[ -z "$1" ] && (echo "$send_notification" | grep -q "router_temp") && soc_temp >"$output_dir/cputemp" &

	if [ -z "$1" ] && (echo "$send_notification" | grep -q "wan_info"); then
		getip wanipv4 >"$output_dir/send_wanIP" &
		getip hostipv4 | jq -r '.IP' >"$output_dir/send_hostIP" &
		waninfo_enable="1"
		if [ "$get_ipv6_mode" -ne "0" ]; then
			getip wanipv6 >"$output_dir/send_wanIPv6" &
			getip hostipv6 | jq -r '.IP' >"$output_dir/send_hostIPv6" &
			ipv6_enable="1"
		fi
		getinterfaceuptime >"$output_dir/interfaceuptime" &
	fi

	#[ -z "$1" ] && ( echo "$send_notification"|grep -q "disk_info" ) && get_disk &
	[ -z "$1" ] && get_disk &

	[ -z "$1" ] && send_title="$(translate "Router status:")"
	[ -n "$1" ] && send_title="$(translate "Send test:")" && send_content="\
${str_splitline}\
${str_title_start}$(translate "Content 1")${str_title_end}\
${str_linefeed}\
${str_tab} - $(translate "Device 1")\
${str_linefeed}\
${str_tab} - $(translate "Device 2")\
${str_splitline}\
${str_title_start}$(translate "Content 2")${str_title_end}\
${str_linefeed}\
${str_tab} - $(translate "Device 3")\
${str_linefeed}\
${str_tab} - $(translate "Device 4")"

	wait_and_cat
	[ -z "$1" ] && (echo "$send_notification" | grep -q "client_list") && current_device & # 设备列表需等待 first & 完成

	[ "$disturb_RETVAL" -ne "0" ] && log_change "${disturb_text} $(translate "Do not disturb time, exit scheduled push")" && return
	if [ -z "$1" ] && [ -n "$systeminfo_enable" ]; then
		[[ $Qwai -eq 200 || $Qwai -eq 301 || $Qwai -eq 302 ]] && Qwai_status="$(translate "Connected!")" || Qwai_status="$(translate "Disconnected!")"
		send_content="${send_content}\
${str_splitline}\
${str_title_start}$(translate "System running status")${str_title_end}\
${str_linefeed}\
${str_tab} - $(translate "Average load:") ${systemload}\
${str_linefeed}\
${str_tab} - $(translate "CPU usage:") ${cpu_load}\
${str_linefeed}\
${str_tab} - $(translate "Memory usage:") ${mem_usage}"
		if [[ "$lang" == "zh_cn" ]]; then
			send_content="${send_content}\
${str_linefeed}\
${str_tab} - $(translate "Global connection:")${Qwai_status}"
		fi
		send_content="${send_content}\
${str_linefeed}\
${str_tab} - ${system_status_time}"
	fi

	if [ -z "$1" ] && (echo "$send_notification" | grep -q "router_temp"); then
		[ -n "$cputemp" ] && send_content="${send_content}\
${str_splitline}\
${str_title_start}$(translate "Device temperature")${str_title_end}\
${str_linefeed}\
${str_tab} - $(translate "CPU:") ${cputemp}℃"
		[ -z "$cputemp" ] && send_content="${send_content}\
${str_splitline}\
${str_title_start}$(translate "Device temperature")${str_title_end}\
${str_linefeed}\
${str_tab} - $(translate "Unable to get device temperature")"
	fi

	#if [ -z "$1" ] && ( echo "$send_notification"|grep -q "disk_info" ); then
	if [ -z "$1" ]; then
		send_content="${send_content}${get_disk}"
	fi

	if [ -z "$1" ] && [ -n "$waninfo_enable" ]; then
		send_content="${send_content}\
${str_splitline}\
${str_title_start}$(translate "WAN Port Information")${str_title_end}"

		if [ "$send_wanIP" == "$send_hostIP" ]; then
			send_content="${send_content}\
${str_linefeed}\
${str_tab} - IPv4: ${send_wanIP}"
		elif [ "$get_ipv4_mode" -eq "1" ]; then
			send_content="${send_content}\
${str_linefeed}\
${str_tab} - $(translate "Interface IPv4:") ${send_wanIP}"
			[ -n "$send_hostIP" ] && send_content="${send_content}\
${str_linefeed}\
${str_tab} - $(translate "Outbound IPv4:") ${send_hostIP}"
		elif [ "$get_ipv4_mode" -eq "2" ]; then
			[ -n "$send_wanIP" ] && send_content="${send_content}\
${str_linefeed}\
${str_tab} - $(translate "Interface IPv4:") ${send_wanIP}"
			send_content="${send_content}\
${str_linefeed}\
${str_tab} - $(translate "Outbound IPv4:") ${send_hostIP}"
		fi

		if [ -n "$ipv6_enable" ]; then
			if [ "$send_wanIPv6" == "$send_hostIPv6" ]; then
				send_content="${send_content}\
${str_linefeed}\
${str_tab} - IPv6: ${send_wanIPv6}"
			elif [ "$get_ipv6_mode" -eq "1" ]; then
				send_content="${send_content}\
${str_linefeed}\
${str_tab} - $(translate "Interface IPv6:") ${send_wanIPv6}"
				[ -n "$send_hostIPv6" ] && send_content="${send_content}\
${str_linefeed}\
${str_tab} - $(translate "Outbound IPv6:") ${send_hostIPv6}"
			elif [ "$get_ipv6_mode" -eq "2" ]; then
				[ -n "$send_wanIPv6" ] && send_content="${send_content}\
${str_linefeed}\
${str_tab} - $(translate "Interface IPv6:") ${send_wanIPv6}"
				send_content="${send_content}\
${str_linefeed}\
${str_tab} - $(translate "Outbound IPv6:") ${send_hostIPv6}"
			fi
		fi

		interface_uptime=$(getinterfaceuptime)
		[ -n "$interface_uptime" ] && wanstatustime=$(translate "Uptime: %dd %dh %dm %ds" "$((interface_uptime / 86400))" "$(((interface_uptime % 86400) / 3600))" "$(((interface_uptime % 3600) / 60))" "$((interface_uptime % 60))")
		send_content="${send_content}\
${str_linefeed}\
${str_tab} - ${wanstatustime}"
	fi

	if [ -z "$1" ] && (echo "$send_notification" | grep -q "client_list"); then
		wait_and_cat
		send_content="${send_content}${current_device}"
	fi

	[ -n "$device_name" ] && send_title="【$device_name】${send_title}"
	[ -z "$send_content" ] && send_content="\
${str_splitline}\
${str_title_start}$(translate "I encountered a problem")${str_title_end}\
${str_linefeed}\
${str_tab} - $(translate "Scheduled send option error, you didn't select any items to send, what should I do?")\
${str_splitline}"

	diy_send "${send_title}" "${send_content}" "${jsonpath}" "$1"
	RETVAL=$?
	[ $RETVAL -eq 1 ] && log_change "[ERROR] $(translate "Scheduled push failed, please check network or settings")" || log_change "${disturb_text} $(translate "Scheduled push task completed")"
	deltemp
	rm -f "${dir}/send_pid"
	return $RETVAL
}

# 登录提醒通知
login_send() {
	local login_ip=$1
	local login_time=$2
	local log_type=$3

	local login_title
	local login_content

	>"${dir}/send_enable.lock"

	[ -z "$login_ip" ] && return
	echo "$login_ip_white_list" | grep -w -q "$login_ip" && return

	[[ "$log_type" == "web"* ]] && local log_type_short="Web" || local log_type_short="SSH"
	[ -f "$logfile" ] && login_log=$(grep -w "$login_ip" "$logfile" | grep -v "\【info\】" | tail -n 1)
	[ -n "$login_log" ] && log_timestamp=$(date -d "$(echo "$login_log" | awk '{print $1, $2}')" +%s) || log_timestamp=0
	if echo "$login_ip_white_list" | grep -w -q "$login_ip" || ( [ -n "$login_disturb" ] && [ "$login_disturb" -eq 1 ] ); then
		log_change "[INFO] $(translate "Device %s logged into router via %s" "$login_ip" "$log_type_short")" && return
	elif [ -n "$login_disturb" ] && [ "$login_disturb" -eq 2 ]; then
		[ $(($(date +%s) - log_timestamp)) -lt $login_notification_delay ] && [ -n "$login_log_enable" ] && return
	fi

	# 查询 IP 归属地
	local login_ip_attribution=$(get_ip_attribution "${login_ip}")
	# 登录方式
	if [[ "$log_type" == "web"* ]]; then
		# Web 登录、非法登录
		local login_mode=$(logread notice | grep -E ".* $login_time.*$login_ip.*" | awk '{print $13}' | tail -n 1)
		[ "$login_mode" = "/" ] && login_mode="$(translate "/ (Homepage login)")"
	elif [[ "$log_type" == "ssh_login"* ]]; then
		# SSH 登录
		local login_mode=$(logread notice | grep -E ".* $login_time.*$login_ip.*" | awk '{print $8}' | tail -n 1)
	else
		local login_mode=$(logread notice | grep -E ".* $login_time.*$login_ip.*" | awk '{for(i=8;i<NF;i++) if($i=="from") break; else printf $i " "}' | tail -n 1)
	fi

	if [ -z "$login_disturb" ] || [ "$login_disturb" -ne "1" ]; then
		if [[ "$log_type" == *"failed"* ]]; then
			local login_title=$(translate "%s frequent %s login attempts" "$login_ip" "$log_type_short")
			local login_content_info="${str_splitline}${str_title_start}$(translate "Block Information")${str_title_end}"
			log_change "${disturb_text} $(translate "Device %s (%s) frequently attempted %s %s login" "$login_ip" "$login_ip_attribution" "$log_type_short" "$login_mode")"
		else
			local login_title=$(translate "%s logged into router via %s" "$login_ip" "$log_type_short")
			local login_content_info="${str_splitline}${str_title_start}$(translate "Login Information")${str_title_end}"
			log_change "${disturb_text} $(translate "Device %s (%s) logged into router via %s %s" "$login_ip" "$login_ip_attribution" "$log_type_short" "$login_mode")"
		fi

		local login_content_time="${str_linefeed}${str_tab} - $(translate "Time:")\
${str_space}${str_space}${str_space}${str_space}${login_time}"
		local login_content_ip="${str_linefeed}${str_tab} - $(translate "Device IP:")\
${str_space}${str_space}${login_ip}"
		[ -n "$login_ip_attribution" ] && local login_content_attribution="${str_linefeed}${str_tab} - $(translate "Location:")\
${str_space}${str_space}${login_ip_attribution}"
		local login_content_mode="${str_linefeed}${str_tab} - $(translate "Login Method:")\
${str_space}${log_type_short} ${login_mode}"

		login_content="${login_content_info}${login_content_time}${login_content_ip}${login_content_attribution}${login_content_mode}"
	fi

	# 发送通知
	disturb
	disturb_RETVAL=$?
	[ -z "$login_title" ] && return
	[ -n "$device_name" ] && login_title="【$device_name】$login_title"
	(echo "$lite_enable" | grep -q "content") && login_content="$login_title"
	[ "$disturb_RETVAL" -eq 0 ] && [ -n "$login_title" ] && diy_send "${login_title}" "${login_content}" "${jsonpath}" >/dev/null 2>&1
}

# 设备离线通知
down_send() {
	# 如果没有离线设备，退出
	[ "$(jq '.devices | map(select(.status == "offline")) | length' "$devices_json")" -eq 0 ] && return

	local IPLIST=$(jq -r '.devices[] | select(.status == "offline") | .ip' "$devices_json" | sort -u)
	for ip in $IPLIST; do
		local mac=$(getmac "$ip")
		blackwhitelist "$mac"
		local ip_blackwhite=$?
		[ -z "$notification_offline" ] || [ -z "$ip_blackwhite" ] && continue
		[ -z "$ip_blackwhite" ] || [ "$ip_blackwhite" -ne "0" ] && continue

		local name=$(getname "$ip" "$mac")
		local time_up=$(jq -r --arg ip "$ip" '.devices[] | select(.ip == $ip and .status == "offline") | .uptime' "$devices_json")
		local ip_total=$(usage get "$mac")
		[ -n "$ip_total" ] && ip_total="${str_linefeed}${str_tab} - $(translate "Total Traffic:") ${str_space}${str_space}${str_space}${str_space}${ip_total}"
		local time1=$(date +%s)
		local time1=$(time_for_humans $(expr ${time1} - ${time_up}))

		if [ -z "$title" ]; then
			title=$(translate "%s disconnected" "$name")
			content_title="${str_splitline}${str_title_start}$(translate "Device Disconnected")${str_title_end}"
		elif echo "$title" | grep -q "$(translate "disconnected")"; then
			title="${name} ${title}"
			content_title=""
		else
			title="$(translate "Device Status Changed")"
			content_title="${str_splitline}${str_title_start}$(translate "Device Disconnected")${str_title_end}"
		fi

		content="${content}\
${content_title}\
${str_linefeed}${str_tab} - $(translate "Client Name:")${str_space}${str_space}${str_space}${str_space}${str_space}${name}\
${str_linefeed}${str_tab} - $(translate "Client IP:")${str_space}${str_space}${str_space}${str_space}${ip}\
${str_linefeed}${str_tab} - $(translate "Client MAC:")${str_space}${str_space}${str_space}${str_space}${mac}${ip_total}\
${str_linefeed}${str_tab} - $(translate "Uptime:")${str_space}${str_space}${str_space}${str_space}${time1}"

		log_change "${disturb_text} $(translate "Device %s %s disconnected" "$name" "$ip")"

	done
	silent_run LockFile lock
	jq 'del(.devices[] | select(.status == "offline"))' "$devices_json" >"$tmp_devices_json" && mv "$tmp_devices_json" "$devices_json"
	silent_run LockFile unlock
}

# 推送
diy_send() {
	[ -z "$jsonpath" ] && return
	(! echo "$lite_enable" | grep -q "content") && (! echo "$lite_enable" | grep -q "nowtime") && local nowtime=$(date "+%Y-%m-%d %H:%M:%S")

	! jq -r '.' ${3} >/dev/null 2>&1 && log_change "[ERROR] $(translate "JSON format error, not a standard JSON file, please check %s for unescaped special characters or syntax errors" "$3")" && return 1

	local diyurl=$(jq -r .url ${3}) && local diyurl=$(eval echo ${diyurl})
	local type=$(jq -r '.type' ${3}) && local type=$(eval echo ${type})
	local data=$(jq -r '.data' ${3}) && local data=$(eval echo ${data})
	local content_type=$(jq -r '.content_type' ${3})

	! jq "$type" ${3} >${tempjsonpath} && log_change "[ERROR] $(translate "type:{ } field format error after variable substitution, please check for unescaped special characters or syntax errors in type:{ } field")" && return 1

	if [[ "$diyurl" =~ ^https?:// ]]; then
		[ -n "$proxy_address" ] && local proxy_cmd="-x $proxy_address"
		run_with_tag curl $proxy_cmd --connect-timeout 30 --max-time 60 --retry 2 -X POST -H "$content_type" -d "$data" "${diyurl}"
	else
		local command=$(jq -r '.command' ${3}) && local command=$(eval echo ${command})
		run_with_tag jq -r "$command" "$data" | eval "$diyurl"
	fi
	local RETVAL=$?

	if [ $RETVAL -ne 0 ] || [[ -n "$4" && "$4" == "test" ]]; then
		[ $RETVAL -ne 0 ] && log_change "[ERROR] $(translate "Network or URL error, push failed, curl returned %s, please check the following debug information" "$RETVAL")"
		[ -n "$4" ] && log_change "[DEBUG] $(translate "Push test enabled, current curl return value is %s" "$RETVAL")"

		echo '{"url":"'${diyurl}'","content_type":"'${content_type}'","type":'$(jq "$type" ${3})'}' >${dir}/debug_send_json
		echo -e "${send_title}" "${send_content}" >${dir}/debug_send_content
		cat ${tempjsonpath} >${dir}/debug_send_data

		! jq -r '.' ${dir}/debug_send_json && log_change "[ERROR] $(translate "Format error after variable substitution, please check %s for unescaped special characters or syntax errors" "$dir")"

		log_change "[DEBUG] $(translate "JSON file saved to: %s" "$dir")"
		log_change "[DEBUG] $(translate "Push content preview saved to: %s" "$dir")"
		log_change "[DEBUG] $(translate "If no message received, please check %s or use the following command to test manually" "$dir")"
		log_change "[DEBUG] curl -X POST -H \"$content_type\" -d \"@${dir}/debug_send_data\" \"${diyurl}\""
		return 1
	else
		return 0
	fi
}

# ------------------------------------
main "$@"
